build: add linux-amd64 + freebsd-amd64 cross-compile from macOS

ober

608216ee00aa5d9cfea0d8cc29e8a5b4dc061d76

diff --git a/.gitignore b/.gitignore
index e7522b4..0abee24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,24 @@
 *.wpo
 *.boot
 *.o
+*.tarm64osx
 !jerboa-aws-main.c
 /jerboa-aws
+/jerboa_aws_program.h
+/jerboa_aws_petite_boot.h
+/jerboa_aws_scheme_boot.h
+/jerboa_aws_app_boot.h
+
+# Cross-compile artifacts
+/jerboa-aws-linux-amd64
+/jerboa-aws-linux-amd64-main.c
+/jerboa-aws-linux-amd64.wp.so
+/jerboa-aws-freebsd-amd64
+/jerboa-aws-freebsd-amd64-main.c
+/jerboa-aws-freebsd-amd64.wp.so
+/petite_boot.h
+/scheme_boot.h
+/jaws_program.h
+
+.claude/
+.DS_Store
diff --git a/Makefile b/Makefile
index 8511f3c..c77f13d 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,14 @@ CHEZSSL ?= $(HOME)/mine/chez-ssl/src
 LIBDIRS = lib:$(JERBOA):$(GHERKIN):$(CHEZHTTPS):$(CHEZSSL)
 COMPILE = $(SCHEME) -q --libdirs $(LIBDIRS) --compile-imported-libraries
 
-.PHONY: all compile binary build run run-pssm clean help test pssm
+# Cross-compile uses libdirs minus gherkin (unused by jerboa-aws code).
+XC_LIBDIRS = lib:$(JERBOA):$(CHEZHTTPS):$(CHEZSSL)
+MUSL_OPENSSL_DIR ?= $(HOME)/musl-openssl
+FREEBSD_SYSROOT  ?= $(HOME)/freebsd-sysroot
+
+.PHONY: all compile binary build run run-pssm clean help test pssm \
+        linux linux-amd64 freebsd freebsd-amd64 \
+        jerboa-aws-linux-amd64 jerboa-aws-freebsd-amd64
 
 all: compile
 
@@ -45,13 +52,63 @@ clean:
 	rm -f jerboa_aws_scheme_boot.h jerboa_aws_app_boot.h
 	rm -f jerboa-aws-all.so aws.so aws.wpo jerboa-aws.boot
 	rm -f pssm
+	rm -f jerboa-aws-linux-amd64   jerboa-aws-linux-amd64-main.c   jerboa-aws-linux-amd64.wp.so
+	rm -f jerboa-aws-freebsd-amd64 jerboa-aws-freebsd-amd64-main.c jerboa-aws-freebsd-amd64.wp.so
+	rm -f petite_boot.h scheme_boot.h jaws_program.h
+
+# ── Cross-compile targets ──────────────────────────────────────────────────
+# Linux x86_64 musl (static): bundles chez-ssl/chez-https + AWS code into a
+# single ELF. Requires:
+#   - $(JERBOA_HOME)/.chez-cross-ta6le      (cross-built Chez install)
+#   - x86_64-linux-musl-gcc                  on PATH
+#   - $(MUSL_OPENSSL_DIR)/usr/{lib,include}  (Alpine static libssl.a + libcrypto.a)
+#   - $(CHEZSSL)/../chez_ssl_shim-linux-musl.o
+#
+linux:        linux-amd64
+linux-amd64:  jerboa-aws-linux-amd64
+jerboa-aws-linux-amd64:
+	@command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { \
+	  echo "Error: x86_64-linux-musl-gcc not on PATH. brew install FiloSottile/musl-cross/musl-cross"; exit 1; }
+	@test -d $(JERBOA_HOME)/.chez-cross-ta6le || { \
+	  echo "Error: $(JERBOA_HOME)/.chez-cross-ta6le missing. cd $(JERBOA_HOME) && make binary"; exit 1; }
+	@test -f $(MUSL_OPENSSL_DIR)/usr/lib/libssl.a || { \
+	  echo "Error: $(MUSL_OPENSSL_DIR)/usr/lib/libssl.a missing."; exit 1; }
+	@test -f $(CHEZSSL)/../chez_ssl_shim-linux-musl.o || { \
+	  echo "Error: chez_ssl_shim-linux-musl.o missing. cd $(CHEZSSL)/.. && make chez_ssl_shim-linux-musl.o"; exit 1; }
+	JERBOA_HOME=$(JERBOA_HOME) MUSL_OPENSSL_DIR=$(MUSL_OPENSSL_DIR) \
+	  $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jaws-cross.ss
+
+# FreeBSD 14 amd64 (dynamic): libssl/libcrypto resolved from FreeBSD base at
+# runtime. Requires:
+#   - $(JERBOA_HOME)/.chez-cross-ta6fb       (cross-built Chez install)
+#   - x86_64-unknown-freebsd14-clang         wrapper on PATH
+#   - $(FREEBSD_SYSROOT)                     (extracted base.txz)
+#   - $(CHEZSSL)/../chez_ssl_shim-freebsd-amd64.o
+#
+freebsd:        freebsd-amd64
+freebsd-amd64:  jerboa-aws-freebsd-amd64
+jerboa-aws-freebsd-amd64:
+	@command -v x86_64-unknown-freebsd14-clang >/dev/null 2>&1 || { \
+	  echo "Error: x86_64-unknown-freebsd14-clang wrapper not on PATH. See ~/.local/bin"; exit 1; }
+	@test -d $(JERBOA_HOME)/.chez-cross-ta6fb || { \
+	  echo "Error: $(JERBOA_HOME)/.chez-cross-ta6fb missing. cd $(JERBOA_HOME) && make binary"; exit 1; }
+	@test -d $(FREEBSD_SYSROOT) || { \
+	  echo "Error: $(FREEBSD_SYSROOT) missing. Extract base.txz to ~/freebsd-sysroot"; exit 1; }
+	@test -f $(CHEZSSL)/../chez_ssl_shim-freebsd-amd64.o || { \
+	  echo "Error: chez_ssl_shim-freebsd-amd64.o missing. cd $(CHEZSSL)/.. && make chez_ssl_shim-freebsd-amd64.o"; exit 1; }
+	JERBOA_HOME=$(JERBOA_HOME) FREEBSD_SYSROOT=$(FREEBSD_SYSROOT) \
+	  $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jaws-freebsd-cross.ss
 
 help:
 	@echo "Targets:"
-	@echo "  all       - Compile all modules"
-	@echo "  build     - Build standalone binary (./jerboa-aws)"
-	@echo "  pssm      - Build pssm wrapper script (./pssm)"
-	@echo "  run       - Run interpreted (ARGS='ec2 describe-instances')"
-	@echo "  run-pssm  - Run pssm interpreted (ARGS=\"'web-*' uptime\")"
-	@echo "  test      - Run tests"
-	@echo "  clean     - Remove build artifacts"
+	@echo "  all                       - Compile all modules"
+	@echo "  build                     - Build standalone binary (./jerboa-aws)"
+	@echo "  pssm                      - Build pssm wrapper script (./pssm)"
+	@echo "  run                       - Run interpreted (ARGS='ec2 describe-instances')"
+	@echo "  run-pssm                  - Run pssm interpreted (ARGS=\"'web-*' uptime\")"
+	@echo "  test                      - Run tests"
+	@echo "  clean                     - Remove build artifacts"
+	@echo ""
+	@echo "Cross-compile from macOS:"
+	@echo "  linux-amd64               - Build jerboa-aws-linux-amd64 (static musl)"
+	@echo "  freebsd-amd64             - Build jerboa-aws-freebsd-amd64 (dynamic)"
diff --git a/build-binary.ss b/build-binary.ss
index 3f9a062..7e703b8 100644
--- a/build-binary.ss
+++ b/build-binary.ss
@@ -28,22 +28,24 @@
     (printf "  ~a: ~a bytes~n" output-path size)))
 
 ;; --- Locate Chez install directory ---
+(define (find-csv-dir lib-dir mt)
+  (let lp ((dirs (guard (e (#t '())) (directory-list lib-dir))))
+    (cond
+      ((null? dirs) #f)
+      ((and (> (string-length (car dirs)) 3)
+            (string=? "csv" (substring (car dirs) 0 3)))
+       (let ((d (format "~a/~a/~a" lib-dir (car dirs) mt)))
+         (and (file-exists? (format "~a/main.o" d)) d)))
+      (else (lp (cdr dirs))))))
+
 (define chez-dir
   (or (getenv "CHEZ_DIR")
-      (let* ((mt (symbol->string (machine-type)))
+      (let* ((mt   (symbol->string (machine-type)))
              (home (getenv "HOME"))
-             (lib-dir (format "~a/.local/lib" home))
-             (csv-dir
-               (let lp ((dirs (guard (e (#t '())) (directory-list lib-dir))))
-                 (cond
-                   ((null? dirs) #f)
-                   ((and (> (string-length (car dirs)) 3)
-                         (string=? "csv" (substring (car dirs) 0 3)))
-                    (format "~a/~a/~a" lib-dir (car dirs) mt))
-                   (else (lp (cdr dirs)))))))
-        (and csv-dir
-             (file-exists? (format "~a/main.o" csv-dir))
-             csv-dir))))
+             (jerboa-home (or (getenv "JERBOA_HOME")
+                              (and home (format "~a/mine/jerboa" home)))))
+        (or (and jerboa-home (find-csv-dir (format "~a/.chez/lib" jerboa-home) mt))
+            (and home        (find-csv-dir (format "~a/.local/lib" home) mt))))))
 
 (unless chez-dir
   (display "Error: Cannot find Chez install dir. Set CHEZ_DIR.\n")
diff --git a/build-jaws-cross.ss b/build-jaws-cross.ss
new file mode 100644
index 0000000..93ba2cb
--- /dev/null
+++ b/build-jaws-cross.ss
@@ -0,0 +1,268 @@
+#!chezscheme
+;;; build-jaws-cross.ss — Cross-compile jerboa-aws from macOS arm64 to Linux x86_64 musl.
+;;;
+;;; Usage:
+;;;   JERBOA_HOME=/Users/user/mine/jerboa scheme -q --libdirs <libs> \
+;;;     --script build-jaws-cross.ss
+;;;
+;;; Uses:
+;;;   - $JERBOA_HOME/.chez-cross-ta6le/   — cross-built Chez install (ta6le)
+;;;   - $JERBOA_HOME/build/chez/xc-ta6le/s/xpatch — host -> ta6le emit-mode loader
+;;;   - x86_64-linux-musl-gcc             — C compile + final static link
+;;;   - ~/mine/chez-ssl/chez_ssl_shim-linux-musl.o  (built by chez-ssl Makefile)
+;;;   - ~/musl-openssl/usr/{lib,include}   — Alpine static libssl.a + libcrypto.a
+;;;
+;;; Produces: jerboa-aws-linux-amd64 (static Linux x86_64 ELF).
+;;;
+;;; Architecture: WPO-as-program (single .wp.so loaded via Sscheme_program). The
+;;; chez-ssl C surface (chez_ssl_*/chez_tcp_*) is statically linked from
+;;; chez_ssl_shim.o and registered via Sforeign_symbol so a -static binary
+;;; resolves it without dlsym.
+
+(import (chezscheme))
+
+;; ── Params ──────────────────────────────────────────────────────────────────
+(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 output       "jerboa-aws-linux-amd64")
+(define entry-script "aws.ss")
+
+(define aws-repo (current-directory))
+(define chez-ssl-repo
+  (or (getenv "CHEZSSL_REPO") (format "~a/mine/chez-ssl" (getenv "HOME"))))
+(define chez-https-repo
+  (or (getenv "CHEZHTTPS_REPO") (format "~a/mine/chez-https" (getenv "HOME"))))
+(define chez-ssl-shim-o
+  (or (getenv "CHEZ_SSL_SHIM_LINUX_O")
+      (format "~a/chez_ssl_shim-linux-musl.o" chez-ssl-repo)))
+(define musl-openssl-dir
+  (or (getenv "MUSL_OPENSSL_DIR") (format "~a/musl-openssl" (getenv "HOME"))))
+
+(define cross-csv-dir
+  (let ([lib (format "~a/lib" cross-prefix)])
+    (unless (file-directory? lib)
+      (error 'build-jaws-cross "cross prefix lib dir missing" 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 'build-jaws-cross "no csv* in cross lib" lib))
+      (format "~a/~a/ta6le" lib (car csvs)))))
+
+(define (require-file p)
+  (unless (file-exists? p)
+    (error 'build-jaws-cross "missing file" p)))
+
+(require-file xpatch)
+(require-file (format "~a/libkernel.a"  cross-csv-dir))
+(require-file (format "~a/scheme.h"     cross-csv-dir))
+(require-file (format "~a/petite.boot"  cross-csv-dir))
+(require-file (format "~a/scheme.boot"  cross-csv-dir))
+(require-file entry-script)
+(require-file chez-ssl-shim-o)
+(require-file (format "~a/usr/lib/libssl.a"    musl-openssl-dir))
+(require-file (format "~a/usr/lib/libcrypto.a" musl-openssl-dir))
+
+(printf "==> build-jaws-cross~n")
+(printf "    JERBOA_HOME:     ~a~n" jerboa-home)
+(printf "    cross csv-dir:   ~a~n" cross-csv-dir)
+(printf "    xpatch:          ~a~n" xpatch)
+(printf "    cross-cc:        ~a~n" cross-cc)
+(printf "    chez-ssl shim:   ~a~n" chez-ssl-shim-o)
+(printf "    musl openssl:    ~a~n" musl-openssl-dir)
+(printf "    output:          ~a~n" output)
+(printf "~n")
+
+;; ── Stage 1: load xpatch (target=ta6le emit mode) ──────────────────────────
+;; xpatch reset library-directories; restore + extend with our source libdirs.
+(define orig-libdirs (library-directories))
+(printf "==> [1/6] loading xpatch (compiler -> ta6le emit mode)~n")
+(load xpatch)
+(library-directories
+  (append
+    (list (cons (format "~a/lib" aws-repo)        (format "~a/lib" aws-repo))
+          (cons (format "~a/lib" jerboa-home)     (format "~a/lib" jerboa-home))
+          (cons (format "~a/src" chez-https-repo) (format "~a/src" chez-https-repo))
+          (cons (format "~a/src" chez-ssl-repo)   (format "~a/src" chez-ssl-repo)))
+    orig-libdirs))
+
+(compile-imported-libraries #t)
+(generate-wpo-files #t)
+
+;; ── Stage 2: compile-program aws.ss ────────────────────────────────────────
+(printf "==> [2/6] compile-program ~a~n" entry-script)
+(compile-program entry-script)
+
+(define entry-wpo
+  (let ([n (string-length entry-script)])
+    (string-append (substring entry-script 0 (- n 3)) ".wpo")))
+
+;; ── Stage 3: compile-whole-program → wpo .so ───────────────────────────────
+(define wpo-output (string-append output ".wp.so"))
+(printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output)
+(compile-whole-program entry-wpo wpo-output #t)
+
+;; ── Stage 4: embed boot files + program as C arrays ────────────────────────
+(define (embed-as-c-array in-path var-name out-path)
+  (let* ([bv (call-with-port (open-file-input-port in-path) get-bytevector-all)]
+         [n (bytevector-length bv)])
+    (call-with-port (open-file-output-port out-path
+                       (file-options no-fail)
+                       (buffer-mode block)
+                       (native-transcoder))
+      (lambda (out)
+        (display (format "static const unsigned char ~a[] = {\n" var-name) out)
+        (let loop ([i 0])
+          (when (< i n)
+            (display (format "0x~2,'0x," (bytevector-u8-ref bv i)) out)
+            (when (= (mod (+ i 1) 16) 0) (newline out))
+            (loop (+ i 1))))
+        (when (positive? n) (newline out))
+        (display "};\n" out)
+        (display (format "static const unsigned int ~a_size = sizeof(~a);\n"
+                         var-name var-name)
+                 out)))
+    (printf "    embed ~a (~a bytes) -> ~a~n" in-path n out-path)))
+
+(printf "==> [4/6] embed boot files + program as C arrays~n")
+(embed-as-c-array (format "~a/petite.boot" cross-csv-dir) "petite_boot" "petite_boot.h")
+(embed-as-c-array (format "~a/scheme.boot" cross-csv-dir) "scheme_boot" "scheme_boot.h")
+(embed-as-c-array wpo-output                              "jaws_program" "jaws_program.h")
+
+;; ── Stage 5: generate main.c ───────────────────────────────────────────────
+(define main-c-path (string-append output "-main.c"))
+
+;; chez-ssl/chez_ssl_shim.c symbol surface — must match the foreign-procedure
+;; declarations in chez-ssl.sls.
+(define chez-ssl-symbols
+  '("chez_ssl_init"        "chez_ssl_cleanup"
+    "chez_ssl_connect"     "chez_ssl_write"      "chez_ssl_read"
+    "chez_ssl_read_all"    "chez_ssl_free_buf"   "chez_ssl_close"
+    "chez_ssl_memcpy"
+    "chez_ssl_server_ctx"  "chez_ssl_server_accept" "chez_ssl_server_ctx_free"
+    "chez_tcp_listen"      "chez_tcp_accept"
+    "chez_tcp_connect"     "chez_tcp_close"
+    "chez_tcp_read"        "chez_tcp_write"      "chez_tcp_read_all"
+    "chez_tcp_set_timeout"
+    "chez_tcp_conn_wrap"
+    "chez_conn_write"      "chez_conn_read"))
+
+(printf "==> [5/6] generate ~a (~a chez-ssl symbols)~n"
+        main-c-path (length chez-ssl-symbols))
+
+(define (write-main-c)
+  (call-with-port
+    (open-file-output-port main-c-path
+      (file-options no-fail) (buffer-mode block) (native-transcoder))
+    (lambda (out)
+      (display "/* Auto-generated by build-jaws-cross.ss. Do not edit. */\n" out)
+      (display "#define _GNU_SOURCE\n" out)
+      (display "#include <stdio.h>\n" out)
+      (display "#include <stdlib.h>\n" out)
+      (display "#include <string.h>\n" out)
+      (display "#include <unistd.h>\n" out)
+      (display "#include <sys/mman.h>\n" out)
+      (display "#include <fcntl.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 \"jaws_program.h\"\n\n" out)
+
+      ;; dlopen stubs (musl-static has no dlopen)
+      (display "/* dlopen stubs for static linking. Returning non-NULL for any\n" out)
+      (display " * named load makes (load-shared-object \"...\") succeed silently;\n" out)
+      (display " * foreign-procedure lookup uses Sforeign_symbol-registered entries. */\n" out)
+      (display "void *dlopen(const char *f, int flags) { (void)f; (void)flags; return (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 stubbed\";\n" out)
+      (display "char *dlerror(void) { return dlerror_msg; }\n\n" out)
+
+      ;; extern decls for chez-ssl symbols
+      (display "/* chez_ssl_shim.o exports — registered with Sforeign_symbol below. */\n" out)
+      (for-each
+        (lambda (sym) (display (format "extern void ~a(void);\n" sym) out))
+        chez-ssl-symbols)
+      (newline out)
+
+      (display "int main(int argc, char *argv[]) {\n" out)
+      (display "    /* Argv passthrough via env vars (matches native jerboa-aws-main.c). */\n" out)
+      (display "    char countbuf[32];\n" out)
+      (display "    snprintf(countbuf, sizeof(countbuf), \"%d\", argc - 1);\n" out)
+      (display "    setenv(\"AWS_ARGC\", countbuf, 1);\n" out)
+      (display "    for (int i = 1; i < argc; i++) {\n" out)
+      (display "        char name[32];\n" out)
+      (display "        snprintf(name, sizeof(name), \"AWS_ARG%d\", i - 1);\n" out)
+      (display "        setenv(name, argv[i], 1);\n" out)
+      (display "    }\n\n" out)
+
+      ;; Memfd-backed program path
+      (display "    int fd = memfd_create(\"jerboa-aws-program\", MFD_CLOEXEC);\n" out)
+      (display "    if (fd < 0) { perror(\"memfd_create\"); return 1; }\n" out)
+      (display "    if (write(fd, jaws_program, jaws_program_size) != (ssize_t)jaws_program_size) {\n" out)
+      (display "        perror(\"write memfd\"); close(fd); return 1;\n" out)
+      (display "    }\n" out)
+      (display "    char prog_path[64];\n" out)
+      (display "    snprintf(prog_path, sizeof(prog_path), \"/proc/self/fd/%d\", fd);\n\n" out)
+
+      (display "    Sscheme_init(NULL);\n" out)
+      (display "    Sregister_boot_file_bytes(\"petite\", (void*)petite_boot, petite_boot_size);\n" out)
+      (display "    Sregister_boot_file_bytes(\"scheme\", (void*)scheme_boot, scheme_boot_size);\n\n" out)
+
+      (display "    /* Register chez_ssl_shim symbols so a -static binary resolves\n" out)
+      (display "     * (foreign-procedure \"chez_ssl_*\" ...) without dlsym. */\n" out)
+      (for-each
+        (lambda (sym)
+          (display (format "    Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym) out))
+        chez-ssl-symbols)
+      (newline out)
+
+      (display "    Sbuild_heap(NULL, NULL);\n" out)
+      (display "    const char *script_args[] = { argv[0] };\n" out)
+      (display "    int status = Sscheme_program(prog_path, 1, script_args);\n\n" out)
+      (display "    close(fd);\n" out)
+      (display "    Sscheme_deinit();\n" out)
+      (display "    return status;\n" out)
+      (display "}\n" out))))
+(write-main-c)
+
+;; ── Stage 6: compile + link with cross-cc ──────────────────────────────────
+(printf "==> [6/6] compile + link with ~a~n" cross-cc)
+
+(define link-cmd
+  (format
+    (string-append
+      "~a -static -O2 "
+      "-I~a -I~a/usr/include "
+      "~a ~a "
+      "-Wl,--start-group "
+      "~a/libkernel.a ~a/libz.a ~a/liblz4.a "
+      "~a/usr/lib/libssl.a ~a/usr/lib/libcrypto.a "
+      "-lpthread -lm -ldl "
+      "-Wl,--end-group "
+      "-o ~a")
+    cross-cc
+    cross-csv-dir musl-openssl-dir
+    main-c-path chez-ssl-shim-o
+    cross-csv-dir cross-csv-dir cross-csv-dir
+    musl-openssl-dir musl-openssl-dir
+    output))
+
+(printf "    ~a~n" link-cmd)
+(unless (zero? (system link-cmd))
+  (error 'build-jaws-cross "link failed"))
+
+(printf "~n==> done: ~a~n" output)
+(printf "    file size: ~a bytes~n"
+        (call-with-port (open-file-input-port output)
+          (lambda (p)
+            (let loop ([n 0])
+              (let ([b (get-u8 p)])
+                (if (eof-object? b) n (loop (+ n 1))))))))
diff --git a/build-jaws-freebsd-cross.ss b/build-jaws-freebsd-cross.ss
new file mode 100644
index 0000000..3cf5713
--- /dev/null
+++ b/build-jaws-freebsd-cross.ss
@@ -0,0 +1,250 @@
+#!chezscheme
+;;; build-jaws-freebsd-cross.ss — Cross-compile jerboa-aws from macOS to FreeBSD 14 amd64.
+;;;
+;;; Usage:
+;;;   JERBOA_HOME=/Users/user/mine/jerboa scheme -q --libdirs <libs> \
+;;;     --script build-jaws-freebsd-cross.ss
+;;;
+;;; Uses:
+;;;   - $JERBOA_HOME/.chez-cross-ta6fb/   — cross-built Chez install (ta6fb)
+;;;   - $JERBOA_HOME/build/chez/xc-ta6fb/s/xpatch — host -> ta6fb emit-mode loader
+;;;   - x86_64-unknown-freebsd14-clang     — wrapper with sysroot + lld + libpath
+;;;   - ~/mine/chez-ssl/chez_ssl_shim-freebsd-amd64.o
+;;;   - ~/freebsd-sysroot/usr/lib/{libssl,libcrypto,...}.so (dynamic link)
+;;;
+;;; Produces: jerboa-aws-freebsd-amd64 (dynamic FreeBSD x86_64 ELF). Dynamic
+;;; because FreeBSD libc.so uses symbol versioning that libc.a/libc_nonshared.a
+;;; can't satisfy. libssl/libcrypto resolved from FreeBSD base at run time.
+
+(import (chezscheme))
+
+;; ── Params ──────────────────────────────────────────────────────────────────
+(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 output       "jerboa-aws-freebsd-amd64")
+(define entry-script "aws.ss")
+
+(define aws-repo (current-directory))
+(define chez-ssl-repo
+  (or (getenv "CHEZSSL_REPO") (format "~a/mine/chez-ssl" (getenv "HOME"))))
+(define chez-https-repo
+  (or (getenv "CHEZHTTPS_REPO") (format "~a/mine/chez-https" (getenv "HOME"))))
+(define chez-ssl-shim-o
+  (or (getenv "CHEZ_SSL_SHIM_FREEBSD_O")
+      (format "~a/chez_ssl_shim-freebsd-amd64.o" chez-ssl-repo)))
+(define freebsd-sysroot
+  (or (getenv "FREEBSD_SYSROOT") (format "~a/freebsd-sysroot" (getenv "HOME"))))
+
+(define cross-csv-dir
+  (let ([lib (format "~a/lib" cross-prefix)])
+    (unless (file-directory? lib)
+      (error 'build-jaws-freebsd-cross "cross prefix lib dir missing" 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 'build-jaws-freebsd-cross "no csv* in cross lib" lib))
+      (format "~a/~a/ta6fb" lib (car csvs)))))
+
+(define (require-file p)
+  (unless (file-exists? p)
+    (error 'build-jaws-freebsd-cross "missing file" p)))
+
+(require-file xpatch)
+(require-file (format "~a/libkernel.a"  cross-csv-dir))
+(require-file (format "~a/scheme.h"     cross-csv-dir))
+(require-file (format "~a/petite.boot"  cross-csv-dir))
+(require-file (format "~a/scheme.boot"  cross-csv-dir))
+(require-file entry-script)
+(require-file chez-ssl-shim-o)
+(require-file (format "~a/usr/lib/libssl.so" freebsd-sysroot))
+
+(printf "==> build-jaws-freebsd-cross~n")
+(printf "    JERBOA_HOME:       ~a~n" jerboa-home)
+(printf "    cross csv-dir:     ~a~n" cross-csv-dir)
+(printf "    xpatch:            ~a~n" xpatch)
+(printf "    cross-cc:          ~a~n" cross-cc)
+(printf "    chez-ssl shim:     ~a~n" chez-ssl-shim-o)
+(printf "    freebsd sysroot:   ~a~n" freebsd-sysroot)
+(printf "    output:            ~a~n" output)
+(printf "~n")
+
+;; ── Stage 1: load xpatch (target=ta6fb emit mode) ──────────────────────────
+(define orig-libdirs (library-directories))
+(printf "==> [1/6] loading xpatch (compiler -> ta6fb emit mode)~n")
+(load xpatch)
+(library-directories
+  (append
+    (list (cons (format "~a/lib" aws-repo)        (format "~a/lib" aws-repo))
+          (cons (format "~a/lib" jerboa-home)     (format "~a/lib" jerboa-home))
+          (cons (format "~a/src" chez-https-repo) (format "~a/src" chez-https-repo))
+          (cons (format "~a/src" chez-ssl-repo)   (format "~a/src" chez-ssl-repo)))
+    orig-libdirs))
+
+(compile-imported-libraries #t)
+(generate-wpo-files #t)
+
+;; ── Stage 2: compile-program aws.ss ────────────────────────────────────────
+(printf "==> [2/6] compile-program ~a~n" entry-script)
+(compile-program entry-script)
+
+(define entry-wpo
+  (let ([n (string-length entry-script)])
+    (string-append (substring entry-script 0 (- n 3)) ".wpo")))
+
+;; ── Stage 3: compile-whole-program → wpo .so ───────────────────────────────
+(define wpo-output (string-append output ".wp.so"))
+(printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output)
+(compile-whole-program entry-wpo wpo-output #t)
+
+;; ── Stage 4: embed boot files + program as C arrays ────────────────────────
+(define (embed-as-c-array in-path var-name out-path)
+  (let* ([bv (call-with-port (open-file-input-port in-path) get-bytevector-all)]
+         [n (bytevector-length bv)])
+    (call-with-port (open-file-output-port out-path
+                       (file-options no-fail)
+                       (buffer-mode block)
+                       (native-transcoder))
+      (lambda (out)
+        (display (format "static const unsigned char ~a[] = {\n" var-name) out)
+        (let loop ([i 0])
+          (when (< i n)
+            (display (format "0x~2,'0x," (bytevector-u8-ref bv i)) out)
+            (when (= (mod (+ i 1) 16) 0) (newline out))
+            (loop (+ i 1))))
+        (when (positive? n) (newline out))
+        (display "};\n" out)
+        (display (format "static const unsigned int ~a_size = sizeof(~a);\n"
+                         var-name var-name)
+                 out)))
+    (printf "    embed ~a (~a bytes) -> ~a~n" in-path n out-path)))
+
+(printf "==> [4/6] embed boot files + program as C arrays~n")
+(embed-as-c-array (format "~a/petite.boot" cross-csv-dir) "petite_boot" "petite_boot.h")
+(embed-as-c-array (format "~a/scheme.boot" cross-csv-dir) "scheme_boot" "scheme_boot.h")
+(embed-as-c-array wpo-output                              "jaws_program" "jaws_program.h")
+
+;; ── Stage 5: generate main.c ───────────────────────────────────────────────
+(define main-c-path (string-append output "-main.c"))
+
+(define chez-ssl-symbols
+  '("chez_ssl_init"        "chez_ssl_cleanup"
+    "chez_ssl_connect"     "chez_ssl_write"      "chez_ssl_read"
+    "chez_ssl_read_all"    "chez_ssl_free_buf"   "chez_ssl_close"
+    "chez_ssl_memcpy"
+    "chez_ssl_server_ctx"  "chez_ssl_server_accept" "chez_ssl_server_ctx_free"
+    "chez_tcp_listen"      "chez_tcp_accept"
+    "chez_tcp_connect"     "chez_tcp_close"
+    "chez_tcp_read"        "chez_tcp_write"      "chez_tcp_read_all"
+    "chez_tcp_set_timeout"
+    "chez_tcp_conn_wrap"
+    "chez_conn_write"      "chez_conn_read"))
+
+(printf "==> [5/6] generate ~a (~a chez-ssl symbols)~n"
+        main-c-path (length chez-ssl-symbols))
+
+(define (write-main-c)
+  (call-with-port
+    (open-file-output-port main-c-path
+      (file-options no-fail) (buffer-mode block) (native-transcoder))
+    (lambda (out)
+      (display "/* Auto-generated by build-jaws-freebsd-cross.ss. Do not edit. */\n" out)
+      (display "#include <stdio.h>\n" out)
+      (display "#include <stdlib.h>\n" out)
+      (display "#include <string.h>\n" out)
+      (display "#include <unistd.h>\n" out)
+      (display "#include <fcntl.h>\n" out)
+      (display "#include <sys/types.h>\n" out)
+      (display "#include <sys/sysctl.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 \"jaws_program.h\"\n\n" out)
+
+      ;; FreeBSD: __errno_location alias (glibc/musl name) -> __error (FreeBSD).
+      (display "/* glibc/musl name for errno location -> FreeBSD __error. */\n" out)
+      (display "extern int *__error(void);\n" out)
+      (display "int *__errno_location(void) { return __error(); }\n\n" out)
+
+      ;; extern decls for chez-ssl symbols
+      (display "/* chez_ssl_shim.o exports — registered with Sforeign_symbol below. */\n" out)
+      (for-each
+        (lambda (sym) (display (format "extern void ~a(void);\n" sym) out))
+        chez-ssl-symbols)
+      (newline out)
+
+      (display "int main(int argc, char *argv[]) {\n" out)
+      (display "    /* Argv passthrough via env vars (matches native jerboa-aws-main.c). */\n" out)
+      (display "    char countbuf[32];\n" out)
+      (display "    snprintf(countbuf, sizeof(countbuf), \"%d\", argc - 1);\n" out)
+      (display "    setenv(\"AWS_ARGC\", countbuf, 1);\n" out)
+      (display "    for (int i = 1; i < argc; i++) {\n" out)
+      (display "        char name[32];\n" out)
+      (display "        snprintf(name, sizeof(name), \"AWS_ARG%d\", i - 1);\n" out)
+      (display "        setenv(name, argv[i], 1);\n" out)
+      (display "    }\n\n" out)
+
+      ;; FreeBSD: no /proc/self/fd/N, write program to a tmpfile instead.
+      (display "    /* FreeBSD has no /proc/self/fd/N (without fdescfs mounted), so write the\n" out)
+      (display "     * embedded program to a tmpfile that we unlink after Sscheme_program loads it. */\n" out)
+      (display "    char tmpl[] = \"/tmp/jerboa-aws-XXXXXX\";\n" out)
+      (display "    int fd = mkstemp(tmpl);\n" out)
+      (display "    if (fd < 0) { perror(\"mkstemp\"); return 1; }\n" out)
+      (display "    if (write(fd, jaws_program, jaws_program_size) != (ssize_t)jaws_program_size) {\n" out)
+      (display "        perror(\"write tmpfile\"); close(fd); unlink(tmpl); return 1;\n" out)
+      (display "    }\n" out)
+      (display "    fsync(fd);\n\n" out)
+
+      (display "    Sscheme_init(NULL);\n" out)
+      (display "    Sregister_boot_file_bytes(\"petite\", (void*)petite_boot, petite_boot_size);\n" out)
+      (display "    Sregister_boot_file_bytes(\"scheme\", (void*)scheme_boot, scheme_boot_size);\n\n" out)
+
+      (display "    /* Register chez_ssl_shim symbols so the WPO resolves them at runtime. */\n" out)
+      (for-each
+        (lambda (sym)
+          (display (format "    Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym) out))
+        chez-ssl-symbols)
+      (newline out)
+
+      (display "    Sbuild_heap(NULL, NULL);\n" out)
+      (display "    const char *script_args[] = { argv[0] };\n" out)
+      (display "    int status = Sscheme_program(tmpl, 1, script_args);\n\n" out)
+      (display "    close(fd);\n" out)
+      (display "    unlink(tmpl);\n" out)
+      (display "    Sscheme_deinit();\n" out)
+      (display "    return status;\n" out)
+      (display "}\n" out))))
+(write-main-c)
+
+;; ── Stage 6: compile + link with cross-cc ──────────────────────────────────
+;; FreeBSD dynamic link: libssl/libcrypto from base, no -static (libc symbol
+;; versioning), libutil for openpty (pulled in by libssl? safe to include).
+(printf "==> [6/6] compile + link with ~a~n" cross-cc)
+
+(define link-cmd
+  (format
+    (string-append
+      "~a -O2 -Wl,--export-dynamic "
+      "-I~a "
+      "~a ~a "
+      "~a/libkernel.a ~a/libz.a ~a/liblz4.a "
+      "-lssl -lcrypto -lpthread -lm -lutil "
+      "-o ~a")
+    cross-cc
+    cross-csv-dir
+    main-c-path chez-ssl-shim-o
+    cross-csv-dir cross-csv-dir cross-csv-dir
+    output))
+
+(printf "    ~a~n" link-cmd)
+(unless (zero? (system link-cmd))
+  (error 'build-jaws-freebsd-cross "link failed"))
+
+(printf "~n==> done: ~a~n" output)