Restore cross-compilation: binary=native, linux/freebsd=cross-only
ober
86e57796bcd036e44d3777ac91066f28149237a3
--- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ else JCODE_OS_LIBS := -lm -ldl -lpthread -luuid -lncurses -lstdc++ endif -.PHONY: all help build gen run test test-providers clean repl binary install tui-shim run-tui native-rs sqlite-bundled linux linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 jcode-linux-native jcode-linux-arm64-native freebsd freebsd-amd64 jcode-freebsd-amd64 jcode-freebsd-native purge-stale sqlite-shim sqlite-lib android android-clean vendor-deps vendor-clean +.PHONY: all help build gen run test test-providers clean repl binary install tui-shim run-tui native-rs sqlite-bundled linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale sqlite-shim sqlite-lib android android-clean vendor-deps vendor-clean all: help @@ -63,10 +63,12 @@ help: @echo " sqlite-bundled Build bundled SQLite archive via cargo" @echo " binary Build native binary via 'jerbuild build' (.jerbuild)" @echo " install Install binary to ~/.local/bin" - @echo " linux Build Linux x86_64 binary with jerbuild (run on Linux)" - @echo " linux-amd64 Build native Linux x86_64 jcode-linux-amd64 with jerbuild" - @echo " linux-arm64 Build native Linux arm64 jcode-linux-arm64 with jerbuild" - @echo " freebsd Build FreeBSD amd64 binary with jerbuild (run on FreeBSD)" + @echo " binary Build jcode for THIS host's os/arch (jerbuild, native)" + @echo " linux Alias for linux-amd64" + @echo " linux-amd64 CROSS-compile static jcode-linux-amd64 (musl) from this host" + @echo " linux-arm64 CROSS-compile static jcode-linux-arm64 (musl) from this host" + @echo " freebsd CROSS-compile dynamic jcode-freebsd-amd64 from this host" + @echo " linux-check Fast drift check of the cross-build (no link, ~10s)" @echo " clean Remove compiled artifacts and binaries" @echo " purge-stale Remove stale .so/.wpo files" @echo " sqlite-shim Rebuild jerboa_sqlite_shim.so" @@ -264,72 +266,104 @@ install: binary fi @echo "Installed to ~/.local/bin/jcode" -# `make linux` builds on a Linux amd64 host through installed jerbuild. +# ─── Cross-compile machinery (host → Linux/FreeBSD) ───────────────────────── +# `make binary` builds for the LOCAL os/arch via jerbuild. The targets below +# CROSS-compile from this host to another platform using the host Chez +# ($(SCHEME)) + a cross-built Chez kernel in $(JERBOA_HOME) + a musl/clang +# cross toolchain. These vars are used ONLY by the cross targets. +JERBOA_HOME ?= $(HOME)/mine/jerboa +SCHEME ?= $(JERBOA_HOME)/.chez/bin/scheme +FREEBSD_AMD64_CC ?= $(JERBOA_HOME)/support/cross-cc-freebsd-amd64 +XC_LIBDIRS = ./lib:vendor/jerboa-sqlite/lib:vendor/jerboa-websearch/src:$(JH)/lib:$(JERBOA_HOME)/lib + +# `make linux` defaults to the linux amd64 cross-build. linux: linux-amd64 -# ─── Linux amd64 binary ───────────────────────────────────────────────────── -# Native Linux build through installed jerbuild. Requires cc and cargo. - -linux-amd64: - @case "$$(uname -s)-$$(uname -m)" in \ - Linux-x86_64|Linux-amd64) ;; \ - *) echo "ERROR: linux-amd64 is a native jerbuild target; run it on Linux amd64" >&2; exit 1 ;; \ - esac - @$(MAKE) jcode-linux-native - -linux-arm64: - @case "$$(uname -s)-$$(uname -m)" in \ - Linux-aarch64|Linux-arm64) ;; \ - *) echo "ERROR: linux-arm64 is a native jerbuild target; run it on Linux arm64/aarch64" >&2; exit 1 ;; \ - esac - @$(MAKE) jcode-linux-arm64-native - -jcode-linux-native: gen $(NATIVE_DIR) - @case "$$(uname -s)-$$(uname -m)" in \ - Linux-x86_64|Linux-amd64) ;; \ - *) echo "ERROR: .jerbuild.linux-amd64 must run on Linux amd64" >&2; exit 1 ;; \ - esac - @echo "=== Building native Linux amd64 jcode with jerbuild ===" - $(JERBUILD) build --config .jerbuild.linux-amd64 +# ── Fast local drift check for the cross-build path ───────────────────────── +# Runs compile-program on main-binary.ss with the same flags the cross build +# uses, but skips the C compile and link. Catches drift (new imports, removed +# stdlib exports) in ~10s before burning minutes in the full cross build. +linux-check: gen + @echo "=== Running linux-check (fast cross-build drift check) ===" + DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \ + $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script linux-check.ss + +# ─── Cross-compile: host → Linux x86_64 / arm64 (musl static) ─────────────── +# Produces a fully static jcode-linux-<arch>. No Docker. Requires: +# - <arch>-linux-musl-gcc on PATH (brew install FiloSottile/musl-cross/musl-cross) +# - $(JERBOA_HOME)/.chez-cross-<m> (cd $(JERBOA_HOME) && make chez-cross[-tarm64le]) +# - rustup target add <arch>-unknown-linux-musl +linux-amd64: gen + @command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { \ + echo "ERROR: x86_64-linux-musl-gcc not found on PATH."; \ + echo "Install with: brew install FiloSottile/musl-cross/musl-cross"; \ + exit 1; } + @test -d $(JERBOA_HOME)/.chez-cross-ta6le || { \ + echo "ERROR: cross-built Chez not found at $(JERBOA_HOME)/.chez-cross-ta6le"; \ + echo "Build it with: cd $(JERBOA_HOME) && make chez-cross"; \ + exit 1; } + @command -v cargo >/dev/null 2>&1 || { \ + echo "ERROR: cargo not found on PATH. Install rustup from rustup.rs"; \ + exit 1; } + JERBOA_HOME=$(JERBOA_HOME) TARGET_ARCH=amd64 $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jcode-cross.ss @ls -lh jcode-linux-amd64 @file jcode-linux-amd64 -jcode-linux-amd64: linux-amd64 - -jcode-linux-arm64-native: gen $(NATIVE_DIR) - @case "$$(uname -s)-$$(uname -m)" in \ - Linux-aarch64|Linux-arm64) ;; \ - *) echo "ERROR: .jerbuild.linux-arm64 must run on Linux arm64/aarch64" >&2; exit 1 ;; \ - esac - @echo "=== Building native Linux arm64 jcode with jerbuild ===" - $(JERBUILD) build --config .jerbuild.linux-arm64 +linux-arm64: gen + @command -v aarch64-linux-musl-gcc >/dev/null 2>&1 || { \ + echo "ERROR: aarch64-linux-musl-gcc not found on PATH."; \ + echo "Install with: brew install FiloSottile/musl-cross/musl-cross"; \ + exit 1; } + @test -d $(JERBOA_HOME)/.chez-cross-tarm64le || { \ + echo "ERROR: cross-built Chez not found at $(JERBOA_HOME)/.chez-cross-tarm64le"; \ + echo "Build it with: cd $(JERBOA_HOME) && make chez-cross-tarm64le"; \ + exit 1; } + @command -v cargo >/dev/null 2>&1 || { \ + echo "ERROR: cargo not found on PATH. Install rustup from rustup.rs"; \ + exit 1; } + JERBOA_HOME=$(JERBOA_HOME) TARGET_ARCH=arm64 $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jcode-cross.ss @ls -lh jcode-linux-arm64 @file jcode-linux-arm64 +# Friendly aliases. +jcode-linux-amd64: linux-amd64 jcode-linux-arm64: linux-arm64 -# ─── FreeBSD amd64 binary ─────────────────────────────────────────────────── -# Native FreeBSD build through installed jerbuild. Requires cc and cargo. - -freebsd-amd64: - @case "$$(uname -s)-$$(uname -m)" in \ - FreeBSD-x86_64|FreeBSD-amd64) ;; \ - *) echo "ERROR: freebsd-amd64 is a native jerbuild target; run it on FreeBSD amd64" >&2; exit 1 ;; \ - esac - @$(MAKE) jcode-freebsd-native - -jcode-freebsd-native: gen $(NATIVE_DIR) - @case "$$(uname -s)-$$(uname -m)" in \ - FreeBSD-x86_64|FreeBSD-amd64) ;; \ - *) echo "ERROR: .jerbuild.freebsd-amd64 must run on FreeBSD amd64" >&2; exit 1 ;; \ - esac - @echo "=== Building native FreeBSD amd64 jcode with jerbuild ===" - $(JERBUILD) build --config .jerbuild.freebsd-amd64 +# Smoke-test the cross-built amd64 binary under alpine x86_64 via podman/qemu. +test-linux-amd64: linux-amd64 + @command -v podman >/dev/null 2>&1 || { \ + echo "ERROR: podman not found. Install with: brew install podman"; exit 1; } + @echo "=== Smoke testing jcode-linux-amd64 under podman/qemu alpine ===" + podman run --rm --platform linux/amd64 \ + -v "$(CURDIR):/work:ro" -w /work \ + alpine:3 ./jcode-linux-amd64 --version + +test-linux: test-linux-amd64 + +# ─── Cross-compile: host → FreeBSD amd64 (dynamic ELF) ────────────────────── +# Produces a dynamic x86_64 FreeBSD ELF (jcode-freebsd-amd64). Requires: +# - $(FREEBSD_AMD64_CC) (macOS clang+lld wrapper) +# - $(JERBOA_HOME)/.chez-cross-ta6fb (cd $(JERBOA_HOME) && make binary) +# - FreeBSD sysroot at $(JERBOA_HOME)/.freebsd-sysroot/amd64 +# - rustup target add x86_64-unknown-freebsd +freebsd-amd64: gen + @command -v $(firstword $(FREEBSD_AMD64_CC)) >/dev/null 2>&1 || { \ + echo "ERROR: $(FREEBSD_AMD64_CC) not found or not executable" >&2; \ + echo " See top of Makefile freebsd-amd64 target for setup notes." >&2; \ + exit 1; } + @test -d $(JERBOA_HOME)/.chez-cross-ta6fb || { \ + echo "ERROR: $(JERBOA_HOME)/.chez-cross-ta6fb not found" >&2; \ + echo " cd $(JERBOA_HOME) && make binary" >&2; \ + exit 1; } + @command -v cargo >/dev/null 2>&1 || { \ + echo "ERROR: cargo not found on PATH. Install rustup from rustup.rs"; \ + exit 1; } + JERBOA_HOME=$(JERBOA_HOME) CROSS_CC="$(FREEBSD_AMD64_CC)" $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jcode-freebsd-cross.ss @ls -lh jcode-freebsd-amd64 @file jcode-freebsd-amd64 +# Friendly aliases. jcode-freebsd-amd64: freebsd-amd64 - freebsd: freebsd-amd64 new file mode 100644 --- /dev/null +++ b/build-jcode-cross.ss @@ -0,0 +1,542 @@ +#!chezscheme +;;; build-jcode-cross.ss — Cross-compile jcode from macOS to Linux musl +;;; +;;; Usage: +;;; JERBOA_HOME=/Users/user/mine/jerboa TARGET_ARCH=amd64 scheme \ +;;; --libdirs <libs> --script build-jcode-cross.ss +;;; +;;; TARGET_ARCH selects the cross target: +;;; amd64 (default) — chez ta6le + x86_64-linux-musl-gcc → jcode-linux-amd64 +;;; arm64 — chez tarm64le + aarch64-linux-musl-gcc → jcode-linux-arm64 +;;; +;;; Cross-compile analogue of build-jcode-musl.ss that runs on a non-Linux host. +;;; Uses: +;;; - $JERBOA_HOME/.chez-cross-<machine>/ — cross-built Chez install +;;; - $JERBOA_HOME/build/chez/xc-<machine>/s/xpatch — host compiler emit mode +;;; - <toolchain>-linux-musl-gcc — C compile + final static link +;;; +;;; This is the "WPO as program" pattern: main-binary.ss + all imports are +;;; bundled into one .so via compile-whole-program, loaded at runtime via +;;; Sscheme_program — no jcode.boot bundling required. + +(import (chezscheme)) + +;; ── Params ────────────────────────────────────────────────────────────────── +(define jerboa-home + (or (getenv "JERBOA_HOME") "/Users/user/mine/jerboa")) + +(define target-arch (or (getenv "TARGET_ARCH") "amd64")) + +;; Per-arch dispatch: (arch chez-machine c-toolchain rust-target output-name) +(define arch-table + '(("amd64" "ta6le" "x86_64-linux-musl-gcc" "x86_64-unknown-linux-musl" "jcode-linux-amd64") + ("arm64" "tarm64le" "aarch64-linux-musl-gcc" "aarch64-unknown-linux-musl" "jcode-linux-arm64"))) + +(define arch-row + (or (assoc target-arch arch-table) + (error 'build-jcode-cross + (format "unknown TARGET_ARCH (expected amd64|arm64): ~a" target-arch)))) + +(define chez-machine (list-ref arch-row 1)) +(define rust-target (list-ref arch-row 3)) + +(define cross-prefix (format "~a/.chez-cross-~a" jerboa-home chez-machine)) +(define xpatch (format "~a/build/chez/xc-~a/s/xpatch" jerboa-home chez-machine)) +(define cross-cc (or (getenv "CROSS_CC") (list-ref arch-row 2))) + +(define output (list-ref arch-row 4)) +(define entry-script "main-binary.ss") + +;; jcode uses only this subset of jerboa-native-rs features: +;; tls — provider/provider.ss (rustls) +;; sqlite — (std db sqlite) via jerboa_sqlite_* +;; crypto — secrets.ss, hashing, hmac, aead, scrypt, argon2id +(define cargo-features "tls,sqlite,crypto") + +(define jerboa-native-a + (or (getenv "JERBOA_NATIVE_A") + (format "~a/jerboa-native-rs/target/~a/release/libjerboa_native.a" + jerboa-home rust-target))) + +(define cross-csv-dir + (let ([lib (format "~a/lib" cross-prefix)]) + (unless (file-directory? lib) + (error 'build-jcode-cross "cross prefix lib dir missing — run 'make chez' in jerboa first" 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-jcode-cross "no csv* in cross lib" lib)) + (format "~a/~a/~a" lib (car csvs) chez-machine)))) + +(define (require-file p) + (unless (file-exists? p) + (error 'build-jcode-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) + +(printf "==> build-jcode-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 " output: ~a~n" output) +(printf " features: ~a~n~n" cargo-features) + +;; ── Step 0: Build/rebuild jerboa-native-rs for x86_64-linux-musl ─────────── +;; libjerboa_native.a is shared across jerboa-shell, jerboa-code, etc. If a +;; sibling cross build (e.g. jsh-cross) built it with a different feature +;; set, rebuild to match what jcode needs. Sentinel file records features. + +(define native-features-sentinel + (format "~a/jerboa-native-rs/target/~a/release/.built-with-~a" + jerboa-home rust-target + (let ([s (string-copy cargo-features)]) + (let loop ([i 0]) + (cond [(= i (string-length s)) s] + [(char=? (string-ref s i) #\,) + (string-set! s i #\-) (loop (+ i 1))] + [else (loop (+ i 1))]))))) + +(define (rs-source-newer-than? a-path) + (let ([src-dir (format "~a/jerboa-native-rs/src" jerboa-home)]) + (and (file-directory? src-dir) + (file-exists? a-path) + (let ([a-mtime (file-modification-time a-path)]) + (let walk ([dirs (list src-dir)]) + (and (pair? dirs) + (let* ([d (car dirs)] + [entries (map (lambda (e) (format "~a/~a" d e)) + (directory-list d))] + [files (filter (lambda (p) (not (file-directory? p))) entries)] + [subs (filter file-directory? entries)]) + (or (ormap (lambda (f) + (and (let ([n (string-length f)]) + (and (> n 3) + (string=? ".rs" (substring f (- n 3) n)))) + (time>? (file-modification-time f) a-mtime))) + files) + (walk (append subs (cdr dirs))))))))))) + +;; Force cargo to use rustup-managed rustc. Homebrew on macOS often ships its +;; own rustc ahead of rustup proxies on PATH; that homebrew rustc has no +;; cross targets installed, so the musl build fails with "can't find crate +;; for core". Detect rustup's active toolchain via `rustup which rustc` and +;; pin both PATH and RUSTC to it (same trick as build-jcode-musl.ss). +(define (path-dirname s) + (let loop ([i (- (string-length s) 1)]) + (cond [(< i 0) "."] + [(char=? (string-ref s i) #\/) (substring s 0 i)] + [else (loop (- i 1))]))) + +(define (capture-line cmd) + ;; open-process-ports returns 4 values: stdin, stdout, stderr, pid. + (call-with-values + (lambda () (open-process-ports cmd (buffer-mode block) (native-transcoder))) + (lambda (to-stdin from-stdout from-stderr pid) + (let ([line (get-line from-stdout)]) + (close-port to-stdin) + (close-port from-stdout) + (close-port from-stderr) + (if (or (eof-object? line) (zero? (string-length line))) #f line))))) + +(define rustup-rustc-path (capture-line "rustup which rustc 2>/dev/null")) +(define rustup-bin-dir (and rustup-rustc-path (path-dirname rustup-rustc-path))) + +(define (try-cargo-build cmd) + (printf " $ ~a~n" cmd) + (zero? (system cmd))) + +(define (rebuild-native-lib!) + (let* ([nrs-dir (format "~a/jerboa-native-rs" jerboa-home)] + [cargo-args (format "build --release --no-default-features --features ~a --target ~a" + cargo-features rust-target)] + [via-rustup + (and rustup-bin-dir + (format "cd '~a' && env PATH='~a':$PATH RUSTC='~a/rustc' '~a/cargo' ~a" + nrs-dir rustup-bin-dir rustup-bin-dir rustup-bin-dir cargo-args))] + [via-path (format "cd '~a' && cargo ~a" nrs-dir cargo-args)] + [ok? (or (and via-rustup (try-cargo-build via-rustup)) + (try-cargo-build via-path))]) + (unless ok? + (error 'build-jcode-cross + (format + (string-append + "failed to build jerboa-native-rs for ~a. " + "Required: `rustup target add ~a`, plus the " + "FiloSottile/musl-cross toolchain on PATH so cargo finds " + "~a as the linker.") + rust-target rust-target cross-cc))) + (unless (file-exists? jerboa-native-a) + (error 'build-jcode-cross "cargo succeeded but .a missing" jerboa-native-a)) + (call-with-output-file native-features-sentinel + (lambda (out) (display cargo-features out) (newline out)) + 'truncate))) + +(cond + [(not (file-exists? jerboa-native-a)) + (printf "==> jerboa-native-rs missing — building (features=~a)~n" cargo-features) + (rebuild-native-lib!)] + [(not (file-exists? native-features-sentinel)) + (printf "==> jerboa-native-rs sentinel missing — rebuilding (features=~a)~n" cargo-features) + (rebuild-native-lib!)] + [(rs-source-newer-than? jerboa-native-a) + (printf "==> jerboa-native-rs .rs source newer than .a — rebuilding~n") + (rebuild-native-lib!)] + [else + (printf "==> jerboa-native-rs up to date (features=~a)~n" cargo-features)]) + +(printf "~n") + +;; ── Step 0.5: isolate cross-compiled objects ─────────────────────────────── +;; Never patch source libraries in-place. Chez xpatch can compile target +;; objects while `library-directories` maps source directories to a separate +;; writable object directory. + +(define obj-dir (format ".build/~a-obj" output)) +(define entry-so (format "~a/program.so" obj-dir)) +(define entry-wpo (format "~a/program.wpo" obj-dir)) + +(define (prepare-obj-dir!) + (system (format "rm -rf '~a'" obj-dir)) + (system (format "mkdir -p '~a'" obj-dir))) + +(define (cleanup!) + (system (format "rm -rf '~a'" obj-dir))) + +(prepare-obj-dir!) + +;; ── Stage 1: load xpatch (target=ta6le emit mode) ────────────────────────── +(define orig-libdirs (library-directories)) +(printf "==> [1/6] loading xpatch (compiler -> ~a emit mode)~n" chez-machine) +(load xpatch) +(library-directories + (map (lambda (pair) + (cons (if (pair? pair) (car pair) pair) obj-dir)) + orig-libdirs)) + +(compile-imported-libraries #t) +(generate-wpo-files #t) + +;; ── Stage 2: compile-program main-binary.ss ──────────────────────────────── +(printf "==> [2/6] compile-program ~a -> ~a~n" entry-script entry-so) +(guard (e [#t (cleanup!) (raise e)]) + (compile-program entry-script entry-so)) + +;; ── 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) +(guard (e [#t (cleanup!) (raise e)]) + (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 "jcode_program" "jcode_program.h") + +;; ── Stage 5: generate main.c ─────────────────────────────────────────────── +;; Symbol surface mirrors build-jcode-musl.ss (FFI + libc), plus the dlopen +;; stubs needed for fully-static binaries. dlerror MUST return non-NULL — +;; Chez's load-shared-object error path strlen()s it. + +(define ffi-shim-symbols + '(;; jerboa-sqlite shim (vendor/jerboa-sqlite/jerboa_sqlite_shim.c) + "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec" + "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset" + "jerboa_sqlite_clear_bindings" "jerboa_sqlite_step" + "jerboa_sqlite_column_count" "jerboa_sqlite_column_name" + "jerboa_sqlite_column_type" "jerboa_sqlite_column_int64" + "jerboa_sqlite_column_double" "jerboa_sqlite_column_text" + "jerboa_sqlite_column_bytes" "jerboa_sqlite_column_blob" + "jerboa_sqlite_bind_int64" "jerboa_sqlite_bind_double" + "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob" + "jerboa_sqlite_bind_null" "jerboa_sqlite_last_insert_rowid" + "jerboa_sqlite_changes" "jerboa_sqlite_errmsg" + "jerboa_SQLITE_ROW" "jerboa_SQLITE_DONE" "jerboa_SQLITE_OK" + ;; termbox2 TUI shim (src/jcode/ui/jcode_tui_shim.c) + "jcode_tb_init" "jcode_tb_shutdown" + "jcode_tb_width" "jcode_tb_height" + "jcode_tb_clear" "jcode_tb_present" + "jcode_tb_set_cursor" "jcode_tb_hide_cursor" + "jcode_tb_change_cell" "jcode_tb_set_clear_attrs" + "jcode_tb_print" "jcode_tb_printf" + "jcode_tb_set_input_mode" "jcode_tb_set_output_mode" + "jcode_tb_poll_event" "jcode_tb_peek_event" + "jcode_tb_event_type" "jcode_tb_event_mod" + "jcode_tb_event_key" "jcode_tb_event_ch" + "jcode_tb_event_w" "jcode_tb_event_h" + "jcode_tb_event_x" "jcode_tb_event_y" + ;; landlock-shim (~/mine/jerboa/support/landlock-shim.c) + "jerboa_landlock_sandbox")) + +(define jerboa-native-symbols + '(;; jerboa-native (TLS/rustls) + "jerboa_tls_server_new" "jerboa_tls_server_new_mtls" + "jerboa_tls_server_free" "jerboa_tls_accept" + "jerboa_tls_connect" "jerboa_tls_connect_pinned" + "jerboa_tls_connect_mtls" "jerboa_tls_close" + "jerboa_tls_read" "jerboa_tls_write" "jerboa_tls_flush" + "jerboa_tls_get_fd" "jerboa_tls_set_nonblock" + "jerboa_last_error" + ;; jerboa-native (crypto) + "jerboa_sha1" "jerboa_sha256" "jerboa_sha384" "jerboa_sha512" + "jerboa_random_bytes" "jerboa_timing_safe_equal" + "jerboa_hmac_sha256" "jerboa_hmac_sha256_verify" + "jerboa_aead_seal" "jerboa_aead_open" + "jerboa_chacha20_seal" "jerboa_chacha20_open" + "jerboa_scrypt" + "jerboa_pbkdf2_derive" "jerboa_pbkdf2_verify" + "jerboa_argon2id_hash" "jerboa_argon2id_verify" + ;; jerboa-native (regex) — visit-time even if jcode never matches + "jerboa_regex_compile" "jerboa_regex_is_match" + "jerboa_regex_find" "jerboa_regex_replace_all" "jerboa_regex_free" + ;; jerboa-native (aproc) — (std os aproc) for bash tool + "jerboa_aproc_spawn" "jerboa_aproc_spawn_pty" + "jerboa_aproc_set_nonblock" "jerboa_aproc_killpg" "jerboa_aproc_wait4" + ;; jerboa-native (sqlite via rusqlite) — used by (std db sqlite) + "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec" + "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset" + "jerboa_sqlite_step" "jerboa_sqlite_changes" "jerboa_sqlite_errmsg" + "jerboa_sqlite_last_insert_rowid" + "jerboa_sqlite_bind_int" "jerboa_sqlite_bind_double" + "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob" "jerboa_sqlite_bind_null" + "jerboa_sqlite_column_count" "jerboa_sqlite_column_type" + "jerboa_sqlite_column_int" "jerboa_sqlite_column_double" + "jerboa_sqlite_column_text" "jerboa_sqlite_column_blob" + "jerboa_sqlite_column_name" + ;; jerboa-native (net/io) + "jerboa_writev2")) + +(define posix-symbols + '(;; sockets — std/net/tcp, std/net/udp + "socket" "bind" "listen" "accept" "connect" "close" + "setsockopt" "getsockopt" "getsockname" + "htons" "ntohs" "inet_pton" "inet_addr" + "read" "write" "recvfrom" "sendto" + ;; DNS — std/net/resolve + "getaddrinfo" "freeaddrinfo" "inet_ntop" + ;; process/signals — std/misc/process, std/os/signal, aproc + "fork" "waitpid" "kill" "getpid" "getppid" + "getpgid" "setpgid" "setsid" + "getuid" "geteuid" "getgid" "getegid" + "sigemptyset" "sigfillset" "sigaddset" "sigdelset" + "sigismember" "sigprocmask" + ;; terminal/fd — TUI + REPL + "isatty" "tcgetattr" "tcsetattr" "tcgetpgrp" "tcsetpgrp" + "pipe" "dup" "dup2" "lseek" + ;; env/misc — std/os/env, std/text/time + "setenv" "unsetenv" "strerror" "localtime" "strftime" + "sysconf" "getpagesize" "getrlimit" + "system" "getenv" "putenv" "_exit" "exit" "execvp" "execve" + ;; temporary files/directories — std/os/temp + "mkstemp" "mkdtemp" "unlink" "rmdir" + ;; errno location + "__errno_location" "fcntl")) + +(define main-c-path (string-append output "-main.c")) + +(define (emit-c out) + (display "/* Generated by build-jcode-cross.ss — do not edit by hand. */\n" out) + (display "#define _GNU_SOURCE\n" out) + (display "#include <stdlib.h>\n" out) + (display "#include <string.h>\n" out) + (display "#include <stdio.h>\n" out) + (display "#include <unistd.h>\n" out) + (display "#include <sys/mman.h>\n" out) + (display "#include <sys/types.h>\n" out) + (display "#include <sys/stat.h>\n" out) + (display "#include <sys/wait.h>\n" out) + (display "#include <sys/resource.h>\n" out) + (display "#include <sys/socket.h>\n" out) + (display "#include <netinet/in.h>\n" out) + (display "#include <arpa/inet.h>\n" out) + (display "#include <netdb.h>\n" out) + (display "#include <termios.h>\n" out) + (display "#include <fcntl.h>\n" out) + (display "#include <signal.h>\n" out) + (display "#include <time.h>\n" out) + (display "#include <errno.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 \"jcode_program.h\"\n\n" out) + ;; dlopen/dlsym stubs — in a fully-static -static binary musl's dlopen + ;; always fails. Jerboa stdlib does (load-shared-object #f) and, in some + ;; libc fallback modules, (load-shared-object "") to expose the main + ;; executable; with these stubs those succeed and Chez routes lookups + ;; through Sforeign_symbol. + (display "/* dlopen stubs for static linking.\n" out) + (display " * (load-shared-object #f) passes f=NULL — succeed (main exe).\n" out) + (display " * (load-shared-object \"\") passes f=\"\" — also succeed (main exe).\n" out) + (display " * (load-shared-object \"libfoo\") passes f=\"libfoo\" — fail so the\n" out) + (display " * Scheme guard falls back to a no-op stub and we never try to\n" out) + (display " * dlsym a function that's not statically linked. */\n" out) + (display "void *dlopen(const char *f, int flags) { (void)flags; return (!f || f[0] == '\\0') ? (void*)1 : NULL; }\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) + ;; dlerror MUST return a non-NULL string — Chez's load-shared-object error + ;; path calls strlen(dlerror()), and strlen(NULL) is a SIGSEGV inside libc + ;; with no useful diagnostic. Return a stable message instead. + (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) + ;; extern decls for shim functions linked via .c files + (display "/* Shim function decls — jerboa_sqlite_shim.c + jcode_tui_shim.c + landlock-shim.c */\n" out) + (for-each (lambda (n) (fprintf out "extern void ~a();\n" n)) ffi-shim-symbols) + ;; extern decls for libjerboa_native.a symbols + (display "\n/* libjerboa_native.a — features=tls,sqlite,crypto */\n" out) + (for-each (lambda (n) (fprintf out "extern void ~a();\n" n)) jerboa-native-symbols) + ;; POSIX libc symbols already declared by system headers above. + (display "\nextern int *__errno_location(void);\n" out) + ;; register all symbols at startup + (newline out) + (display "static void register_ffi_symbols(void) {\n" out) + (for-each (lambda (n) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" n n)) + ffi-shim-symbols) + (for-each (lambda (n) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" n n)) + jerboa-native-symbols) + (for-each (lambda (n) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" n n)) + posix-symbols) + (display "}\n\n" out) + (display "int main(int argc, char *argv[]) {\n" out) + ;; Tell jerboa stdlib we are statically linked — must precede Sscheme_init. + (display " setenv(\"JERBOA_STATIC\", \"1\", 1);\n\n" out) + ;; memfd_create for the WPO program, with tmpfile fallback + (display " int fd = memfd_create(\"jcode-program\", MFD_CLOEXEC);\n" out) + (display " char prog_path[64];\n" out) + (display " if (fd >= 0) {\n" out) + (display " if (write(fd, jcode_program, jcode_program_size) != (ssize_t)jcode_program_size) {\n" out) + (display " perror(\"write memfd\"); close(fd); return 1;\n" out) + (display " }\n" out) + (display " snprintf(prog_path, sizeof(prog_path), \"/proc/self/fd/%d\", fd);\n" out) + (display " } else {\n" out) + (display " const char *tmp = getenv(\"TMPDIR\"); if (!tmp) tmp = \"/tmp\";\n" out) + (display " snprintf(prog_path, sizeof(prog_path), \"%s/.jcode-prog-%d.so\", tmp, getpid());\n" out) + (display " FILE *fp = fopen(prog_path, \"wb\");\n" out) + (display " if (!fp) { perror(\"fopen tmpfile\"); return 1; }\n" out) + (display " if (fwrite(jcode_program, 1, jcode_program_size, fp) != jcode_program_size) {\n" out) + (display " perror(\"fwrite\"); fclose(fp); unlink(prog_path); return 1;\n" out) + (display " }\n" out) + (display " fclose(fp);\n" out) + (display " }\n\n" out) + ;; Boot Chez. + (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" out) + (display " Sbuild_heap(NULL, NULL);\n" out) + (display " register_ffi_symbols();\n\n" out) + ;; argv is passed through directly to Sscheme_program — main-binary.ss + ;; reads (command-line-arguments) for --tui detection. + (display " int status = Sscheme_program(prog_path, argc, (const char **)argv);\n\n" out) + (display " if (fd >= 0) close(fd); else unlink(prog_path);\n" out) + (display " Sscheme_deinit();\n" out) + (display " return status;\n" out) + (display "}\n" out)) + +(call-with-port (open-file-output-port main-c-path + (file-options no-fail) (buffer-mode block) (native-transcoder)) + emit-c) +(printf "==> [5/6] generated ~a (~a shim + ~a native + ~a posix)~n" + main-c-path + (length ffi-shim-symbols) + (length jerboa-native-symbols) + (length posix-symbols)) + +;; ── Stage 6: compile + link with cross-cc ────────────────────────────────── +(printf "==> [6/6] compile + link with ~a~n" cross-cc) + +(define sqlite-shim-c "vendor/jerboa-sqlite/jerboa_sqlite_shim.c") +(define jcode-tui-shim-c "src/jcode/ui/jcode_tui_shim.c") +(define landlock-shim-c (format "~a/support/landlock-shim.c" jerboa-home)) + +(require-file sqlite-shim-c) +(require-file jcode-tui-shim-c) +(require-file landlock-shim-c) +(require-file jerboa-native-a) + +;; The jerboa_sqlite_shim.c calls into sqlite3_* directly (declared via +;; <sqlite3.h>). At link time, those symbols come from libjerboa_native.a +;; which has rusqlite bundling its own sqlite3. We need ANY sqlite3.h for +;; compile, since the public sqlite3 C API is stable across versions — +;; macOS Homebrew's header is fine. Find via brew. +(define brew-sqlite-prefix + (capture-line "brew --prefix sqlite 2>/dev/null")) + +(define sqlite-include-dir + (cond + [(and brew-sqlite-prefix + (file-exists? (format "~a/include/sqlite3.h" brew-sqlite-prefix))) + (format "~a/include" brew-sqlite-prefix)] + [(file-exists? "/usr/include/sqlite3.h") "/usr/include"] + [(file-exists? "/usr/local/include/sqlite3.h") "/usr/local/include"] + [else + (error 'build-jcode-cross + "no sqlite3.h found. Install via: brew install sqlite")])) + +(printf " sqlite3.h: ~a/sqlite3.h~n" sqlite-include-dir) +(printf " tls/sqlite/crypto via libjerboa_native.a~n") + +;; -Wl,--export-dynamic exports symbols into the dynamic symbol table so +;; Chez's foreign-procedure / dlsym(RTLD_DEFAULT) can find them in a +;; -static binary. (Same trick as build-jsh-cross.ss.) +(define link-cmd + (format + (string-append + "~a -O2 -static -Wl,--export-dynamic " + "-I~a " ;; scheme.h + "-I~a " ;; sqlite3.h + "-Ivendor/termbox2 -DTB_OPT_ATTR_W=32 " ;; jcode_tui_shim + "-o ~a " ;; output + "~a ~a ~a ~a " ;; main.c + 3 shims + "~a/libkernel.a ~a/libz.a ~a/liblz4.a " ;; chez kernel + "~a " ;; libjerboa_native.a + "-Wl,--allow-multiple-definition " + "-Wl,--defsym=_dl_find_object=0 " + "-lm -ldl -lpthread") + cross-cc cross-csv-dir sqlite-include-dir output + main-c-path sqlite-shim-c jcode-tui-shim-c landlock-shim-c + cross-csv-dir cross-csv-dir cross-csv-dir + jerboa-native-a)) +(printf " ~a~n" link-cmd) +(let ([rc (system link-cmd)]) + (unless (zero? rc) + (cleanup!) + (error 'build-jcode-cross "cross-link failed" rc))) + +;; ── Cleanup: remove isolated cross objects ───────────────────────────────── +(cleanup!) + +(printf "~n=== Build complete: ~a ===~n" output) +(system (format "ls -lh ~a" output)) +(system (format "file ~a" output)) new file mode 100644 --- /dev/null +++ b/build-jcode-freebsd-cross.ss @@ -0,0 +1,538 @@ +#!chezscheme +;;; build-jcode-freebsd-cross.ss — Cross-compile jcode from macOS arm64 to FreeBSD 14 amd64 +;;; +;;; Usage: +;;; JERBOA_HOME=/Users/user/mine/jerboa scheme --libdirs <libs> \ +;;; --script build-jcode-freebsd-cross.ss +;;; +;;; FreeBSD analogue of build-jcode-cross.ss. Uses: +;;; - $JERBOA_HOME/.chez-cross-ta6fb/ — cross-built Chez install (FreeBSD) +;;; - $JERBOA_HOME/build/chez/xc-ta6fb/s/xpatch — host compiler → ta6fb emit +;;; - $JERBOA_HOME/support/cross-cc-freebsd-amd64 — macOS clang+lld wrapper +;;; +;;; Produces: jcode-freebsd-amd64 (dynamic FreeBSD x86_64 ELF; depends on +;;; libc.so.7, libm.so, libthr.so, libutil.so on the target host). +;;; +;;; Why dynamic and not static: FreeBSD libc uses symbol versioning +;;; (wait4@FBSD_1.0 etc.) that libc.a + libc_nonshared.a from base.txz +;;; cannot satisfy. Dynamic linking is the standard FreeBSD distribution +;;; model anyway. + +(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") + (format "~a/support/cross-cc-freebsd-amd64" jerboa-home))) + +(define output "jcode-freebsd-amd64") +(define entry-script "main-binary.ss") + +;; jcode uses only this subset of jerboa-native-rs features: +;; tls — provider/provider.ss (rustls) +;; sqlite — (std db sqlite) via jerboa_sqlite_* +;; crypto — secrets.ss, hashing, hmac, aead, scrypt, argon2id +(define cargo-features "tls,sqlite,crypto") + +(define jerboa-native-a + (or (getenv "JERBOA_NATIVE_A") + (format "~a/jerboa-native-rs/target/x86_64-unknown-freebsd/release/libjerboa_native.a" + jerboa-home))) + +(define cross-csv-dir + (let ([lib (format "~a/lib" cross-prefix)]) + (unless (file-directory? lib) + (error 'build-jcode-freebsd-cross "cross prefix lib dir missing — run 'make binary' in jerboa first" 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-jcode-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-jcode-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) + +(printf "==> build-jcode-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 " output: ~a~n" output) +(printf " features: ~a~n~n" cargo-features) + +;; ── Step 0: Build/rebuild jerboa-native-rs for x86_64-unknown-freebsd ────── +;; libjerboa_native.a is shared across jerboa-shell, jerboa-code, etc. If a +;; sibling cross build (e.g. jsh-cross) built it with a different feature +;; set, rebuild to match what jcode needs. Sentinel file records features. + +(define native-features-sentinel + (format "~a/jerboa-native-rs/target/x86_64-unknown-freebsd/release/.built-with-~a" + jerboa-home + (let ([s (string-copy cargo-features)]) + (let loop ([i 0]) + (cond [(= i (string-length s)) s] + [(char=? (string-ref s i) #\,) + (string-set! s i #\-) (loop (+ i 1))] + [else (loop (+ i 1))]))))) + +(define (rs-source-newer-than? a-path) + (let ([src-dir (format "~a/jerboa-native-rs/src" jerboa-home)]) + (and (file-directory? src-dir) + (file-exists? a-path) + (let ([a-mtime (file-modification-time a-path)]) + (let walk ([dirs (list src-dir)]) + (and (pair? dirs) + (let* ([d (car dirs)] + [entries (map (lambda (e) (format "~a/~a" d e)) + (directory-list d))] + [files (filter (lambda (p) (not (file-directory? p))) entries)] + [subs (filter file-directory? entries)]) + (or (ormap (lambda (f) + (and (let ([n (string-length f)]) + (and (> n 3) + (string=? ".rs" (substring f (- n 3) n)))) + (time>? (file-modification-time f) a-mtime))) + files) + (walk (append subs (cdr dirs))))))))))) + +;; Force cargo to use rustup-managed rustc. Homebrew on macOS often ships its +;; own rustc ahead of rustup proxies on PATH; that homebrew rustc has no +;; cross targets installed, so the musl build fails with "can't find crate +;; for core". Detect rustup's active toolchain via `rustup which rustc` and +;; pin both PATH and RUSTC to it (same trick as build-jcode-musl.ss). +(define (path-dirname s) + (let loop ([i (- (string-length s) 1)]) + (cond [(< i 0) "."] + [(char=? (string-ref s i) #\/) (substring s 0 i)] + [else (loop (- i 1))]))) + +(define (capture-line cmd) + ;; open-process-ports returns 4 values: stdin, stdout, stderr, pid. + (call-with-values + (lambda () (open-process-ports cmd (buffer-mode block) (native-transcoder))) + (lambda (to-stdin from-stdout from-stderr pid) + (let ([line (get-line from-stdout)]) + (close-port to-stdin) + (close-port from-stdout) + (close-port from-stderr) + (if (or (eof-object? line) (zero? (string-length line))) #f line))))) + +(define rustup-rustc-path (capture-line "rustup which rustc 2>/dev/null")) +(define rustup-bin-dir (and rustup-rustc-path (path-dirname rustup-rustc-path))) + +(define (try-cargo-build cmd) + (printf " $ ~a~n" cmd) + (zero? (system cmd))) + +(define (rebuild-native-lib!) + (let* ([nrs-dir (format "~a/jerboa-native-rs" jerboa-home)] + [cargo-args (format "build --release --no-default-features --features ~a --target x86_64-unknown-freebsd" + cargo-features)] + ;; cc-rs needs LLVM's llvm-ar (not Apple's /usr/bin/ar) and the + ;; FreeBSD clang wrapper. Forward these to the cargo invocation. + [cc-env (format + "CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER='~a' CC_x86_64_unknown_freebsd='~a' AR_x86_64_unknown_freebsd=/opt/homebrew/opt/llvm/bin/llvm-ar " + cross-cc cross-cc)] + [via-rustup + (and rustup-bin-dir + (format "cd '~a' && env ~aPATH='~a':$PATH RUSTC='~a/rustc' '~a/cargo' ~a" + nrs-dir cc-env rustup-bin-dir rustup-bin-dir rustup-bin-dir cargo-args))] + [via-path (format "cd '~a' && env ~acargo ~a" nrs-dir cc-env cargo-args)] + [ok? (or (and via-rustup (try-cargo-build via-rustup)) + (try-cargo-build via-path))]) + (unless ok? + (error 'build-jcode-freebsd-cross + (string-append + "failed to build jerboa-native-rs for x86_64-unknown-freebsd. " + "Required: `rustup target add x86_64-unknown-freebsd`, plus " + cross-cc " and " + "/opt/homebrew/opt/llvm/bin/llvm-ar from the LLVM Homebrew formula."))) + (unless (file-exists? jerboa-native-a) + (error 'build-jcode-freebsd-cross "cargo succeeded but .a missing" jerboa-native-a)) + (call-with-output-file native-features-sentinel + (lambda (out) (display cargo-features out) (newline out)) + 'truncate))) + +(cond + [(not (file-exists? jerboa-native-a)) + (printf "==> jerboa-native-rs missing — building (features=~a)~n" cargo-features) + (rebuild-native-lib!)] + [(not (file-exists? native-features-sentinel)) + (printf "==> jerboa-native-rs sentinel missing — rebuilding (features=~a)~n" cargo-features) + (rebuild-native-lib!)] + [(rs-source-newer-than? jerboa-native-a) + (printf "==> jerboa-native-rs .rs source newer than .a — rebuilding~n") + (rebuild-native-lib!)] + [else + (printf "==> jerboa-native-rs up to date (features=~a)~n" cargo-features)]) + +(printf "~n") + +;; ── Step 0.5: isolate cross-compiled objects ─────────────────────────────── +;; Never patch source libraries in-place. Chez xpatch can compile target +;; objects while `library-directories` maps source directories to a separate +;; writable object directory. + +(define obj-dir (format ".build/~a-obj" output)) +(define entry-so (format "~a/program.so" obj-dir)) +(define entry-wpo (format "~a/program.wpo" obj-dir)) + +(define (prepare-obj-dir!) + (system (format "rm -rf '~a'" obj-dir)) + (system (format "mkdir -p '~a'" obj-dir))) + +(define (cleanup!) + (system (format "rm -rf '~a'" obj-dir))) + +(prepare-obj-dir!) + +;; ── Stage 1: load xpatch (target=ta6le emit mode) ────────────────────────── +(define orig-libdirs (library-directories)) +(printf "==> [1/6] loading xpatch (compiler -> ta6fb emit mode)~n") +(load xpatch) +(library-directories + (map (lambda (pair) + (cons (if (pair? pair) (car pair) pair) obj-dir)) + orig-libdirs)) + +(compile-imported-libraries #t) +(generate-wpo-files #t) + +;; ── Stage 2: compile-program main-binary.ss ──────────────────────────────── +(printf "==> [2/6] compile-program ~a -> ~a~n" entry-script entry-so) +(guard (e [#t (cleanup!) (raise e)]) + (compile-program entry-script entry-so)) + +;; ── 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) +(guard (e [#t (cleanup!) (raise e)]) + (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 "jcode_program" "jcode_program.h") + +;; ── Stage 5: generate main.c ─────────────────────────────────────────────── +;; Symbol surface mirrors build-jcode-musl.ss (FFI + libc), plus the dlopen +;; stubs needed for fully-static binaries. dlerror MUST return non-NULL — +;; Chez's load-shared-object error path strlen()s it. + +(define ffi-shim-symbols + '(;; jerboa-sqlite shim (vendor/jerboa-sqlite/jerboa_sqlite_shim.c) + "jerboa_sqlite_open" "jerboa_sqlite_close" "jerboa_sqlite_exec" + "jerboa_sqlite_prepare" "jerboa_sqlite_finalize" "jerboa_sqlite_reset" + "jerboa_sqlite_clear_bindings" "jerboa_sqlite_step" + "jerboa_sqlite_column_count" "jerboa_sqlite_column_name" + "jerboa_sqlite_column_type" "jerboa_sqlite_column_int64" + "jerboa_sqlite_column_double" "jerboa_sqlite_column_text" + "jerboa_sqlite_column_bytes" "jerboa_sqlite_column_blob" + "jerboa_sqlite_bind_int64" "jerboa_sqlite_bind_double" + "jerboa_sqlite_bind_text" "jerboa_sqlite_bind_blob"