build: cross-compile jcode to FreeBSD 14 x86_64 from non-Linux hosts
ober
dc120c5ebdd64c02d7596084bd12f024151dcfa6
--- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ jcode.json # Cross-compile (make linux-amd64) artifacts /jcode-linux-amd64 /jcode-linux-amd64-main.c +/jcode-freebsd-amd64 +/jcode-freebsd-amd64-main.c /petite_boot.h /scheme_boot.h /jcode_program.h +.claude/ +.jcode/ --- a/Makefile +++ b/Makefile @@ -270,10 +270,44 @@ test-linux-amd64: linux-amd64 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 \ - --script build-binary.ss +# ─── FreeBSD amd64 binary (cross-build from macOS) ────────────────────── +# Produces a dynamic x86_64 FreeBSD ELF (jcode-freebsd-amd64) from a macOS +# host. Depends on the target host's libc.so.7 / libm.so / libthr.so / +# libutil.so at runtime. +# +# 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 at link time. Dynamic linking is the standard FreeBSD +# distribution model anyway. +# +# Prereqs (set up once): +# - cd $(JERBOA_HOME) && make binary (builds .chez-cross-ta6fb/) +# - FreeBSD sysroot at ~/freebsd-sysroot (extracted from base.txz of +# a FreeBSD 14.x release: tar xJf base.txz ./usr/include ./usr/lib ./lib) +# - x86_64-unknown-freebsd14-clang on PATH (wrapper at ~/.local/bin/) +# - rustup target add x86_64-unknown-freebsd (one-time) + +freebsd-amd64: gen + @command -v x86_64-unknown-freebsd14-clang >/dev/null 2>&1 || { \ + echo "ERROR: x86_64-unknown-freebsd14-clang not on PATH" >&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) $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jcode-freebsd-cross.ss + +# Friendly aliases. +jcode-freebsd-amd64: freebsd-amd64 + +# `make freebsd` defaults to the cross-from-macOS path (matches `make linux`). +# For a native-on-FreeBSD build (legacy build-binary.ss code path), run +# `make build` from a FreeBSD host instead. +freebsd: freebsd-amd64 clean: find lib/jcode -name "*.sls" -delete 2>/dev/null; true --- a/build-binary.ss +++ b/build-binary.ss @@ -70,8 +70,13 @@ (define chez-dir (or (getenv "CHEZ_DIR") (let ((mt (symbol->string (machine-type))) - (home (getenv "HOME"))) - (or (find-csv-dir (format "~a/.local/lib" home) mt) + (home (getenv "HOME")) + (jerboa-home (or (getenv "JERBOA_HOME") + (format "~a/mine/jerboa" (getenv "HOME"))))) + ;; Prefer the self-built Chez under $JERBOA_HOME/.chez (canonical + ;; since jerboa commit 3fe0459 — `make binary` there builds it). + (or (find-csv-dir (format "~a/.chez/lib" jerboa-home) mt) + (find-csv-dir (format "~a/.local/lib" home) mt) (find-csv-dir "/usr/local/lib" mt) (find-csv-dir "/usr/lib" mt) (find-csv-dir "/opt/homebrew/lib" mt) new file mode 100644 --- /dev/null +++ b/build-jcode-freebsd-cross.ss @@ -0,0 +1,563 @@ +#!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 +;;; - x86_64-unknown-freebsd14-clang — macOS clang+lld with FreeBSD sysroot +;;; +;;; 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") "x86_64-unknown-freebsd14-clang")) + +(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 (string-append + "CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=x86_64-unknown-freebsd14-clang " + "CC_x86_64_unknown_freebsd=x86_64-unknown-freebsd14-clang " + "AR_x86_64_unknown_freebsd=/opt/homebrew/opt/llvm/bin/llvm-ar ")] + [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 the " + "x86_64-unknown-freebsd14-clang wrapper on PATH 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: 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")) + +;; Linux-only symbols (landlock, etc.) referenced by jerboa stdlib but never +;; exercised on FreeBSD. Emit as `return -1` C stubs so the binary links; +;; the Scheme code never calls them on this platform. +(define freebsd-stub-symbols + '("jerboa_landlock_sandbox" + "jerboa_landlock_abi_version" "jerboa_landlock_create_ruleset" + "jerboa_landlock_add_path_rule" "jerboa_landlock_add_net_rule" + "jerboa_landlock_enforce" + "jerboa_epoll_create" "jerboa_epoll_ctl" "jerboa_epoll_wait" "jerboa_epoll_close" + "jerboa_eventfd_create" "jerboa_eventfd_drain" "jerboa_eventfd_signal" + "jerboa_inotify_init" "jerboa_inotify_add_watch" "jerboa_inotify_rm_watch" + "jerboa_inotify_read" "jerboa_inotify_close" + "jerboa_seccomp_available" "jerboa_seccomp_lock" "jerboa_seccomp_lock_strict" + "jerboa_writev2")) + +(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 is Linux-only (http_parse + ;; module is #[cfg(target_os = "linux")] in jerboa-native-rs/src/lib.rs); + ;; provided as a return-error stub via freebsd-stub-symbols below. + )) + +(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" + "fcntl")) + +(define main-c-path (string-append output "-main.c")) + +(define (emit-c out) + (display "/* Generated by build-jcode-freebsd-cross.ss — do not edit by hand. */\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/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) + ;; Dynamic-linked FreeBSD binary: keep libc's real dlopen/dlsym/dlerror. + ;; (load-shared-object #f) passes NULL, dlsym(RTLD_DEFAULT, ...) finds + ;; main-exe symbols thanks to -Wl,--export-dynamic on the final link. + ;; + ;; errno: FreeBSD libc names the TLS accessor __error, not __errno_location + ;; (glibc/musl). Provide a wrapper and register it under BOTH names so the + ;; jerboa stdlib's Sforeign_symbol("__errno_location", ...) lookup works. + (display "static int *freebsd_errno_location(void) { return &errno; }\n\n" out) + ;; Linux-only Rust symbols — provide returning-error stubs. + (display "/* Linux-only jerboa_* symbols — return -1 stubs on FreeBSD */\n" out) + (for-each (lambda (n) + (fprintf out "static int ~a() { return -1; }\n" n)) + freebsd-stub-symbols) + ;; extern decls for shim functions linked via .c files + (display "\n/* Shim function decls — chez_sqlite_shim.c + jcode_tui_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) + ;; 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)) + freebsd-stub-symbols) + (for-each (lambda (n) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" n n)) + posix-symbols) + ;; errno: register the wrapper under both Linux and FreeBSD names. + (display " Sforeign_symbol(\"__errno_location\", (void*)freebsd_errno_location);\n" out) + (display " Sforeign_symbol(\"__error\", (void*)freebsd_errno_location);\n" out) + (display "}\n\n" out) + (display "int main(int argc, char *argv[]) {\n" out) + ;; Tell jerboa stdlib we are statically linked — must precede Sscheme_init. + ;; (Same env flag the Linux musl build uses; tells the stdlib not to dlopen + ;; named .so libraries since our Sforeign_symbol table covers everything.) + (display " setenv(\"JERBOA_STATIC\", \"1\", 1);\n\n" out) + ;; FreeBSD has no /proc/self/fd/N by default (needs fdescfs mounted), and + ;; memfd_create exists but is useless without /proc. Use tmpfile only. + (display " char prog_path[64];\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 " {\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 " 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 freebsd-stubs + ~a posix)~n" + main-c-path + (length ffi-shim-symbols) + (length jerboa-native-symbols) + (length freebsd-stub-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") +;; landlock-shim is Linux-only (sys/prctl.h, __NR_landlock_create_ruleset); +;; on FreeBSD jerboa_landlock_sandbox is provided by a static stub above. + +(require-file sqlite-shim-c) +(require-file jcode-tui-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-freebsd-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") + +;; FreeBSD link differences from Linux musl: +;; - Dynamic, not static (FreeBSD libc symbol versioning blocks libc.a) +;; - -lpthread = libthr (FreeBSD's POSIX thread library) +;; - -lutil for openpty (Rust pty crate, used by jerboa_aproc_spawn_pty) +;; - No -ldl (dlopen lives in libc on FreeBSD) +;; - No --allow-multiple-definition / _dl_find_object (musl-specific) +;; - -Wl,--export-dynamic exposes main-exe symbols for dlsym(RTLD_DEFAULT) +(define link-cmd + (format + (string-append + "~a -O2 -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 " ;; main.c + 2 shims + "~a/libkernel.a ~a/libz.a ~a/liblz4.a " ;; chez kernel + "~a " ;; libjerboa_native.a + "-lm -lpthread -lutil") + cross-cc cross-csv-dir sqlite-include-dir output + main-c-path sqlite-shim-c jcode-tui-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-freebsd-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))