build: cross-compile jcode to Linux x86_64 from non-Linux hosts
ober
b9161d902c40aca5678983948c3e262ec2e88f79
--- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.so *.wpo +*.tarm64osx /jcode .jerbuild-hashes jcode.json @@ -14,3 +15,10 @@ jcode.json /android/build/ /android/dist/ /android/app/src/main/assets/native/ + +# Cross-compile (make linux-amd64) artifacts +/jcode-linux-amd64 +/jcode-linux-amd64-main.c +/petite_boot.h +/scheme_boot.h +/jcode_program.h --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ TUI_SHIM_DIR := $(CURDIR)/vendor/termbox2 NATIVE_LIB_DIR := $(JERBOA_HOME)/lib LDPATH := $(SHIM_DIR):$(TUI_SHIM_DIR):$(SQLITE_LIB_DIR):$(NATIVE_LIB_DIR) -.PHONY: all help build gen run test test-providers clean repl binary install tui-shim run-tui jcode-musl linux linux-check linux-local docker purge-stale sqlite-shim android android-clean +.PHONY: all help build gen run test test-providers clean repl binary install tui-shim run-tui jcode-musl linux linux-amd64 jcode-linux-amd64 linux-docker linux-check linux-local docker test-linux test-linux-amd64 purge-stale sqlite-shim android android-clean all: help @@ -30,9 +30,12 @@ help: @echo " gen Run jerbuild + rebuild sqlite/TUI shims" @echo " binary Build native binary (macOS)" @echo " install Install binary to ~/.local/bin" - @echo " linux Build static musl binary via Docker" - @echo " linux-local Build static musl binary on host" + @echo " linux Build Linux x86_64 static binary (cross-compile from this host)" + @echo " linux-amd64 Same as 'linux' (explicit arch name)" + @echo " linux-docker Build static musl binary via Docker (slower; canonical)" + @echo " linux-local Build static musl binary on host (Linux only)" @echo " linux-check Fast drift check for musl build path" + @echo " test-linux Smoke-test the cross-compiled binary under podman/qemu" @echo " clean Remove compiled artifacts and binaries" @echo " purge-stale Remove stale .so/.wpo files" @echo " sqlite-shim Rebuild chez_sqlite_shim.so" @@ -90,12 +93,23 @@ sqlite-shim: # # Defer to jerboa's own `make native` so we don't need to know about the # cargo target dir or how the .so gets ad-hoc-signed on macOS. -NATIVE_SO := $(NATIVE_LIB_DIR)/libjerboa_native.so +NATIVE_SO := $(NATIVE_LIB_DIR)/libjerboa_native.so +NATIVE_DYLIB := $(NATIVE_LIB_DIR)/libjerboa_native.dylib native-so: - @if [ ! -f "$(NATIVE_SO)" ]; then \ - echo "=== libjerboa_native.so missing — building via $(JERBOA_HOME) ==="; \ + @if [ ! -f "$(NATIVE_SO)" ] && [ ! -f "$(NATIVE_DYLIB)" ]; then \ + echo "=== libjerboa_native missing — building via $(JERBOA_HOME) ==="; \ $(MAKE) -C $(JERBOA_HOME) native; \ fi + @# On macOS, `make native` produces only .dylib but the jerboa stdlib + @# loaders (std crypto native-rust, std regex-native, std native) only + @# try the .so extension. Symlink so visit-time load-shared-object + @# succeeds. Without this, freshly recompiling those libraries (e.g. + @# after `make purge-stale` or a cross-build) dies with + @# "no entry for jerboa_last_error". + @if [ ! -f "$(NATIVE_SO)" ] && [ -f "$(NATIVE_DYLIB)" ]; then \ + echo "=== symlinking libjerboa_native.so -> libjerboa_native.dylib ==="; \ + ln -sf libjerboa_native.dylib "$(NATIVE_SO)"; \ + fi gen: purge-stale sqlite-shim native-so $(JERBUILD) src lib @@ -155,7 +169,10 @@ install: binary fi @echo "Installed to ~/.local/bin/jcode" -linux: linux-check docker +# `make linux` now defaults to the fast cross-compile path. Use `make +# linux-docker` for the canonical Docker build (reproducible, slower). +linux: linux-amd64 +linux-docker: linux-check docker # ── Fast local drift check for the musl build path ───────────────────────── # Runs compile-program on main-binary.ss with the same flags the musl build @@ -212,6 +229,47 @@ linux-local: linux-check # if you specifically want a native build. jcode-musl: docker +# ─── Cross-compile (macOS / FreeBSD → Linux x86_64 musl) ──────────────────── +# Build a static jcode binary for Linux x86_64 from a non-Linux host. +# No Docker needed — uses Chez xpatch + musl-gcc cross toolchain. +# +# Requires: +# - $(JERBOA_HOME)/.chez-cross-ta6le — cross-built Chez (make chez-cross in jerboa) +# - x86_64-linux-musl-gcc on PATH (brew install FiloSottile/musl-cross/musl-cross) +# - rustup target x86_64-unknown-linux-musl (rustup target add x86_64-unknown-linux-musl) +# +# Produces: jcode-linux-amd64 + +XC_LIBDIRS = $(JERBOA_HOME)/lib:./lib:vendor/chez-sqlite/src:vendor/jerboa-websearch/src + +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) $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jcode-cross.ss + +# Friendly alias — same target, more explicit name. +jcode-linux-amd64: linux-amd64 + +test-linux-amd64: linux-amd64 + @echo "=== Smoke testing jcode-linux-amd64 under podman/qemu alpine ===" + @command -v podman >/dev/null 2>&1 || { \ + echo "ERROR: podman not found. Install with: brew install podman"; \ + exit 1; } + podman run --rm --platform linux/amd64 \ + -v "$(CURDIR):/work:ro" -w /work \ + alpine:3 ./jcode-linux-amd64 --version + +test-linux: test-linux-amd64 + freebsd: build JERBOA_HOME=$(JERBOA_HOME) \ $(SCHEME) -q --libdirs $(JERBOA_HOME)/lib:./lib:vendor/chez-sqlite/src:vendor/jerboa-websearch/src \ new file mode 100644 --- /dev/null +++ b/build-jcode-cross.ss @@ -0,0 +1,543 @@ +#!chezscheme +;;; build-jcode-cross.ss — Cross-compile jcode from macOS arm64 to Linux x86_64 musl +;;; +;;; Usage: +;;; JERBOA_HOME=/Users/user/mine/jerboa scheme --libdirs <libs> \ +;;; --script build-jcode-cross.ss +;;; +;;; Cross-compile analogue of build-jcode-musl.ss that runs on a non-Linux host. +;;; Uses: +;;; - $JERBOA_HOME/.chez-cross-ta6le/ — cross-built Chez install +;;; - $JERBOA_HOME/build/chez/xc-ta6le/s/xpatch — host compiler → ta6le emit +;;; - x86_64-linux-musl-gcc — C compile + final static link +;;; +;;; Produces: jcode-linux-amd64 (static Linux x86_64 ELF, no shared library deps) +;;; +;;; 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 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 "jcode-linux-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-linux-musl/release/libjerboa_native.a" + jerboa-home))) + +(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/ta6le" lib (car csvs))))) + +(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/x86_64-unknown-linux-musl/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-linux-musl" + cargo-features)] + [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 + (string-append + "failed to build jerboa-native-rs for x86_64-unknown-linux-musl. " + "Required: `rustup target add x86_64-unknown-linux-musl`, plus the " + "FiloSottile/musl-cross toolchain on PATH so cargo finds " + "x86_64-linux-musl-gcc as the linker."))) + (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: Patch load-shared-object in libraries for static linking ───── +;; In a static binary, dlopen is stubbed so any (load-shared-object "libfoo.so") +;; raises. Patch all .sls files (jerboa stdlib + vendor + project) to replace +;; the form with (void), matching what build-jcode-musl.ss does. Patches are +;; reverted at end of script via `git checkout`. + +(printf "==> [0.5/6] patching load-shared-object in libraries~n") +(define lso-patch-cmd + (string-append + "perl -i -0777 -pe 's/" + "\\(load-shared-object(?=\\s)" + "(?:[^()\"]++|\"(?:\\\\.|[^\"\\\\])*+\"|" + "(?<bal>\\((?:[^()\"]++|\"(?:\\\\.|[^\"\\\\])*+\"|(?&bal))*+\\))" + ")*+\\)/(void)/g'")) + +(define jerboa-lib-dir (format "~a/lib" jerboa-home)) +(system (format "find '~a' -name '*.sls' -exec ~a {} +" jerboa-lib-dir lso-patch-cmd)) +(system (format "find vendor/chez-sqlite -name '*.sls' -exec ~a {} +" lso-patch-cmd)) +(when (file-directory? "vendor/jerboa-websearch") + (system (format "find vendor/jerboa-websearch -name '*.sls' -exec ~a {} +" lso-patch-cmd))) +(when (file-directory? "lib") + (system (format "find lib -name '*.sls' -exec ~a {} +" lso-patch-cmd))) + +;; Delete stale .so files so they get recompiled with the patches. +(system (format "find '~a' -name '*.so' -delete 2>/dev/null" jerboa-lib-dir)) +(system "find vendor/chez-sqlite -name '*.so' -delete 2>/dev/null") +(system "find vendor/jerboa-websearch -name '*.so' -delete 2>/dev/null") +(system "find lib -name '*.so' -delete 2>/dev/null") + +(define (restore-patched-files!) + ;; Only restore .sls files — step 0.5 only patches .sls (never .ss), so a + ;; broad `git checkout -- .` would clobber unrelated .ss edits like vendor + ;; bug fixes that have to apply before compile-whole-program runs. + (printf "~n==> [cleanup] restoring patched .sls files via git~n") + (system (format "cd '~a' && git ls-files -z -- '*.sls' | xargs -0 git checkout -- 2>/dev/null" jerboa-home)) + (system "git ls-files -z -- 'lib/jcode/*.sls' | xargs -0 git checkout -- 2>/dev/null") + (system "cd vendor/chez-sqlite && git ls-files -z -- 'src/*.sls' | xargs -0 git checkout -- 2>/dev/null") + (when (file-directory? "vendor/jerboa-websearch") + (system "cd vendor/jerboa-websearch && git ls-files -z -- '*.sls' | xargs -0 git checkout -- 2>/dev/null"))) + +;; ── Stage 1: load xpatch (target=ta6le emit mode) ────────────────────────── +(define orig-libdirs (library-directories)) +(printf "==> [1/6] loading xpatch (compiler -> ta6le emit mode)~n") +(load xpatch) +(library-directories orig-libdirs) + +(compile-imported-libraries #t) +(generate-wpo-files #t) + +;; ── Stage 2: compile-program main-binary.ss ──────────────────────────────── +(printf "==> [2/6] compile-program ~a~n" entry-script) +(guard (e [#t (restore-patched-files!) (raise e)]) + (compile-program entry-script)) + +;; ── Stage 3: compile-whole-program → wpo .so ─────────────────────────────── +(define wpo-output (string-append output ".wp.so")) +(printf "==> [3/6] compile-whole-program main-binary.wpo -> ~a~n" wpo-output) +(guard (e [#t (restore-patched-files!) (raise e)]) + (compile-whole-program "main-binary.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 + '(;; chez-sqlite shim (vendor/chez-sqlite/chez_sqlite_shim.c) + "chez_sqlite_open" "chez_sqlite_close" "chez_sqlite_exec" + "chez_sqlite_prepare" "chez_sqlite_finalize" "chez_sqlite_reset" + "chez_sqlite_clear_bindings" "chez_sqlite_step" + "chez_sqlite_column_count" "chez_sqlite_column_name" + "chez_sqlite_column_type" "chez_sqlite_column_int64" + "chez_sqlite_column_double" "chez_sqlite_column_text" + "chez_sqlite_column_bytes" "chez_sqlite_column_blob" + "chez_sqlite_bind_int64" "chez_sqlite_bind_double" + "chez_sqlite_bind_text" "chez_sqlite_bind_blob" + "chez_sqlite_bind_null" "chez_sqlite_last_insert_rowid" + "chez_sqlite_changes" "chez_sqlite_errmsg" + "chez_SQLITE_ROW" "chez_SQLITE_DONE" "chez_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" "execvp" "execve" + ;; 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) at init; with + ;; these stubs that succeeds 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 \"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 ? 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) + ;; 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 — chez_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/chez-sqlite/chez_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 chez_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) + (restore-patched-files!) + (error 'build-jcode-cross "cross-link failed" rc))) + +;; ── Cleanup: restore patched files ────────────────────────────────────────── +(restore-patched-files!) + +;; Clean up intermediate compile-program outputs so the next dev cycle on +;; host scheme doesn't pick up ta6le-flavored .so files. +(for-each (lambda (f) + (when (file-exists? f) (delete-file f))) + '("main-binary.so" "main-binary.wpo")) + +(printf "~n=== Build complete: ~a ===~n" output) +(system (format "ls -lh ~a" output)) +(system (format "file ~a" output)) --- a/vendor/jerboa-websearch +++ b/vendor/jerboa-websearch @@ -1 +1 @@ -Subproject commit fb0643d54081717d2552ffec76ea5fdb89e475fc +Subproject commit c1f19ffbec557de5275d32e64443812a305f8f3e