multicall: cross-compile jerboa binary for linux-amd64 + freebsd-amd64

ober

bf3912bce176fab78f842c47d8ca395a747152b7

diff --git a/Makefile b/Makefile
index cff675c..b1abada 100644
--- a/Makefile
+++ b/Makefile
@@ -45,6 +45,8 @@ help:
 	@echo "  binary-cross     Build a jerboa-bin for a foreign target (same args as chez-cross)"
 	@echo "  jerboa           Build the busybox-style multicall binary: dist/jerboa"
 	@echo "                   + symlinks dist/{jmcp,jlsp,jerbuild} (one image, all four modes)"
+	@echo "  jerboa-portable  Build the multicall binary for host + linux-amd64 + freebsd-amd64"
+	@echo "                   (cross targets need: make chez-cross CHEZ_TARGET_MACHINE=ta6le|ta6fb ...)"
 	@echo "  native-cross     Cross-build Rust native lib for a target"
 	@echo "                   Args: CHEZ_TARGET_MACHINE=<mt> CROSS_CC=<cc> [CROSS_NATIVE_FEATURES=tls,...]"
 	@echo "  typecheck        Typecheck Typed Jerboa sources"
@@ -222,6 +224,56 @@ jerboa-smoke: jerboa
 	rm -f /tmp/jms.ss; \
 	[ $$fail -eq 0 ] && echo "jerboa-smoke: PASS" || { echo "jerboa-smoke: FAIL" >&2; exit 1; }
 
+# ── Cross-compiled multicall binary ──────────────────────────────────────────
+# Builds dist/<os-arch>/{jerboa,jmcp,jlsp,jerbuild} for a foreign target, all
+# four modes runnable on that target. The xpatch retargets the host Chez's
+# codegen; the cross Chez supplies target boot files + libkernel.a.
+#
+# LIMITATION: on-target `jerbuild binary` does NOT work from a cross binary yet.
+# Chez cross-compilation emits host-tagged .wpo (only .so get the target tag),
+# and target-tagged .wpo can only be produced natively ON the target. The
+# running multicall image has the stdlib internalized, so it cannot regenerate
+# them in-process. Enabling self-build needs a bare-Chez subprocess mode (TODO).
+#
+# Generic form (builds the cross Chez on demand via chez-cross):
+#   make jerboa-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc
+.PHONY: jerboa-cross jerboa-linux-amd64 jerboa-freebsd-amd64 jerboa-portable
+jerboa-cross: chez build mcp-check lsp-gen chez-cross
+	@test -n "$(CHEZ_TARGET_MACHINE)" \
+		|| { echo "ERROR: set CHEZ_TARGET_MACHINE=<machine-type>" >&2; exit 1; }
+	JERBOA_HOME=$(JERBOA_HOME) \
+	TARGET_MACHINE=$(CHEZ_TARGET_MACHINE) \
+	JERBOA_CROSS_PREFIX=$(CHEZ_CROSS_PREFIX) \
+	JERBOA_XPATCH=$(CHEZ_XPATCH) \
+	CC="$(CROSS_CC)" \
+	$(SCHEME) --script support/build-jerboa-multicall.ss
+
+# Convenience targets: assume `make chez-cross` already produced the cross Chez.
+jerboa-linux-amd64: chez build mcp-check lsp-gen
+	@command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { echo "ERROR: x86_64-linux-musl-gcc not on PATH" >&2; exit 1; }
+	@test -d "$(JERBOA_HOME)/.chez-cross-ta6le" || { echo "ERROR: .chez-cross-ta6le not found (make chez-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc)" >&2; exit 1; }
+	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch" || { echo "ERROR: ta6le xpatch not found" >&2; exit 1; }
+	@echo "=== Cross-building jerboa multicall: linux-amd64 (ta6le) ==="
+	@JERBOA_HOME=$(JERBOA_HOME) TARGET_MACHINE=ta6le \
+		JERBOA_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6le \
+		JERBOA_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch \
+		CC=x86_64-linux-musl-gcc \
+		$(SCHEME) --script support/build-jerboa-multicall.ss
+
+jerboa-freebsd-amd64: chez build mcp-check lsp-gen
+	@test -x "$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" || { echo "ERROR: support/cross-cc-freebsd-amd64 not found/executable" >&2; exit 1; }
+	@test -d "$(JERBOA_HOME)/.chez-cross-ta6fb" || { echo "ERROR: .chez-cross-ta6fb not found (make chez-cross CHEZ_TARGET_MACHINE=ta6fb CROSS_CC=...)" >&2; exit 1; }
+	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch" || { echo "ERROR: ta6fb xpatch not found" >&2; exit 1; }
+	@echo "=== Cross-building jerboa multicall: freebsd-amd64 (ta6fb) ==="
+	@JERBOA_HOME=$(JERBOA_HOME) TARGET_MACHINE=ta6fb \
+		JERBOA_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6fb \
+		JERBOA_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch \
+		CC="$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" \
+		$(SCHEME) --script support/build-jerboa-multicall.ss
+
+# Host + the two cross targets.
+jerboa-portable: jerboa jerboa-linux-amd64 jerboa-freebsd-amd64
+
 # Cross-compiled jerbuild. Drives support/build-jerbuild.sh with TARGET_* env,
 # producing ./jerbuild-<machine> alongside the host ./jerbuild.
 #
diff --git a/support/build-jerboa-multicall.ss b/support/build-jerboa-multicall.ss
index 8b4d019..bbbaf0c 100644
--- a/support/build-jerboa-multicall.ss
+++ b/support/build-jerboa-multicall.ss
@@ -251,20 +251,61 @@
                                   script
                                   (format "~a/~a" (current-directory) script))])
                     (path-parent (path-parent abs)))))
-(define machine (symbol->string (machine-type)))
-(define csv-dir (or (getenv "CHEZ_DIR")
-                    (find-csv-dir (format "~a/.chez/lib" repo) machine)))
-(unless csv-dir (error 'build-multicall "no Chez csv dir for" machine (format "~a/.chez/lib" repo)))
+;; ── cross-compilation params (mirror support/build-jerbuild.sh) ───────────────
+;; TARGET_MACHINE set => cross build. JERBOA_CROSS_PREFIX is the install prefix
+;; of the cross-built Chez (`.chez-cross-<mt>`, holding lib/csv*/<mt>/...) and
+;; JERBOA_XPATCH is xc-<mt>/s/xpatch, loaded before compile so the host Chez
+;; emits target FASL. All produced by `make chez-cross`.
+(define target-machine (env "TARGET_MACHINE" #f))
+(define cross?       (and target-machine #t))
+(define xpatch       (and cross? (env "JERBOA_XPATCH" #f)))
+(define cross-prefix (and cross? (env "JERBOA_CROSS_PREFIX" #f)))
+(when cross?
+  (unless cross-prefix (error 'build-multicall "TARGET_MACHINE set but JERBOA_CROSS_PREFIX is not"))
+  (unless xpatch       (error 'build-multicall "TARGET_MACHINE set but JERBOA_XPATCH is not"))
+  (unless (file-exists? xpatch) (error 'build-multicall "xpatch not found" xpatch)))
+
+(define machine (if cross? target-machine (symbol->string (machine-type))))
+
+;; Map machine-type suffix -> target OS, and -> a friendly dist subdir label.
+(define (machine->os m)
+  (cond [(string-suffix? m "osx") 'darwin]
+        [(string-suffix? m "fb")  'freebsd]
+        [(string-suffix? m "nt")  'windows]
+        [else 'linux]))
+(define target-os (machine->os machine))
+(define (machine->label m)
+  (cond [(string=? m "ta6le")     "linux-amd64"]
+        [(string=? m "tarm64le")  "linux-arm64"]
+        [(string=? m "ta6fb")     "freebsd-amd64"]
+        [(string=? m "tarm64fb")  "freebsd-arm64"]
+        [(string=? m "ta6osx")    "macos-amd64"]
+        [(string=? m "tarm64osx") "macos-arm64"]
+        [else m]))
 
-(define out-dir  (env "OUTPUT_DIR" (format "~a/dist" repo)))
+(define csv-dir (or (getenv "CHEZ_DIR")
+                    (find-csv-dir (format "~a/lib" (if cross? cross-prefix (format "~a/.chez" repo)))
+                                  machine)))
+(unless csv-dir (error 'build-multicall "no Chez csv dir for" machine
+                       (if cross? cross-prefix (format "~a/.chez/lib" repo))))
+
+(define out-dir  (env "OUTPUT_DIR" (if cross?
+                                       (format "~a/dist/~a" repo (machine->label machine))
+                                       (format "~a/dist" repo))))
 (define output   (format "~a/jerboa" out-dir))
-(define build-dir (format "~a/build/jerboa-multicall" repo))
+(define build-dir (format "~a/build/jerboa-multicall~a" repo
+                          (if cross? (format "-~a" machine) "")))
 (define gen-dir   (format "~a/gen" build-dir))
 (define obj-dir   (format "~a/obj" build-dir))
 (define program-wp-so (format "~a/program.wp.so" build-dir))
+(define libdirs (list (cons gen-dir obj-dir)
+                      (cons (format "~a/lib" repo) obj-dir)
+                      (cons repo obj-dir)))
 
 (printf "=== jerboa multicall build ===~n")
 (printf "    repo:    ~a~n" repo)
+(printf "    mode:    ~a~n" (if cross? (format "CROSS (~a -> ~a, ~a)" (machine-type) target-os machine) "native"))
+(when cross? (printf "    xpatch:  ~a~n" xpatch))
 (printf "    machine: ~a~n" machine)
 (printf "    chez:    ~a~n" csv-dir)
 (printf "    output:  ~a~n" output)
@@ -286,9 +327,15 @@
 (write-unified-entry entry-ss)
 
 (printf "==> [2/6] WPO compile shared image~n")
-(library-directories (list (cons gen-dir obj-dir)
-                           (cons (format "~a/lib" repo) obj-dir)
-                           (cons repo obj-dir)))
+(library-directories libdirs)
+;; Load the xpatch AFTER setting library-directories (it may mutate them) and
+;; re-assert ours, so subsequent compile-* emit target FASL. Safe to do
+;; in-process: this builder's image imports only (chezscheme); no Jerboa libs
+;; are loaded as the host machine-type to collide with the retargeted codegen.
+(when cross?
+  (printf "    loading xpatch ~a~n" xpatch)
+  (load xpatch)
+  (library-directories libdirs))
 (compile-imported-libraries #t)
 (generate-wpo-files #t)
 (compile-program entry-ss (format "~a/program.so" obj-dir))
@@ -324,10 +371,18 @@
   (lambda (o) (fprintf o "static const char bundle_sha256[] = \"~a\";~n" bundle-sha)) 'replace)
 
 (printf "==> [5/6] compile + link -> ~a~n" output)
+;; Cross Chez is configured --disable-curses --disable-x11 --disable-iconv, so
+;; libkernel.a has no ncurses/iconv refs; native host Chez is built --as-is.
 (define os-libs
-  (cond [(string-suffix? machine "osx") "-lm -lpthread -lncurses -liconv"]
-        [(string-suffix? machine "fb")  "-lm -lpthread -lncurses -L/usr/local/lib -liconv"]
-        [else "-lm -ldl -lpthread -lncurses"]))
+  (if cross?
+      (case target-os
+        [(linux)   "-lm -ldl -lpthread -static"]
+        [(freebsd) "-lm -lpthread"]
+        [(darwin)  "-lm -lpthread"]
+        [else      "-lm -lpthread"])
+      (cond [(string-suffix? machine "osx") "-lm -lpthread -lncurses -liconv"]
+            [(string-suffix? machine "fb")  "-lm -lpthread -lncurses -L/usr/local/lib -liconv"]
+            [else "-lm -ldl -lpthread -lncurses"])))
 (define extra-archives
   (apply string-append
          (map (lambda (a) (if (file-exists? (format "~a/~a" csv-dir a)) (format " ~a/~a" csv-dir a) ""))