feat: cross-compile Rust native lib + FreeBSD target

ober

0740adab907fa030dddb37c02fde904db28f2cc7

diff --git a/.gitignore b/.gitignore
index dfff9c0..aa79897 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,12 @@
 /jerboa-bin-*-main.c
 /jerboa-bin-*.wp.so
 
+# `make native-cross` outputs (per-target Rust dylibs)
+/lib-cross/
+
+# FreeBSD sysroot for cross builds (downloaded base.txz extract)
+/.freebsd-sysroot/
+
 # Repo-local Chez build/install artifacts
 /.chez/
 /.chez-cross-*/
diff --git a/Makefile b/Makefile
index d88c7f9..8eb934e 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,7 @@ CHEZ_EXT_LIBDIRS = $(CHEZ_EXT_DIR)/chez-https/src:$(CHEZ_EXT_DIR)/chez-ssl/src:$
 # Shared object paths for legacy FFI-based chez-* libraries
 CHEZ_EXT_LDPATH = $(CHEZ_EXT_DIR)/chez-ssl:$(CHEZ_EXT_DIR)/chez-zlib:$(CHEZ_EXT_DIR)/chez-pcre2:$(CHEZ_EXT_DIR)/chez-leveldb:$(CHEZ_EXT_DIR)/chez-epoll:$(CHEZ_EXT_DIR)/chez-inotify:$(CHEZ_EXT_DIR)/chez-crypto:$(CHEZ_EXT_DIR)/chez-sqlite:$(CHEZ_EXT_DIR)/chez-postgresql
 
-.PHONY: help chez chez-cross build binary binary-cross test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-native test-gaps native clean-native audit-native clean fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-websocket-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push
+.PHONY: help chez chez-cross build binary binary-cross native-cross test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-native test-gaps native clean-native audit-native clean fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-websocket-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push
 
 help:
 	@echo "Usage: make <target>"
@@ -36,6 +36,8 @@ help:
 	@echo "  chez-cross       Build cross Chez (target libkernel.a + boot files + xpatch)"
 	@echo "                   Args: CHEZ_TARGET_MACHINE=<mt> CROSS_CC=<cross-cc>"
 	@echo "  binary-cross     Build a jerboa-bin for a foreign target (same args as chez-cross)"
+	@echo "  native-cross     Cross-build Rust native lib for a target"
+	@echo "                   Args: CHEZ_TARGET_MACHINE=<mt> CROSS_CC=<cc> [CROSS_NATIVE_FEATURES=tls,...]"
 	@echo "  native           Build Rust native library"
 	@echo "  clean            Remove compiled .so and .wpo artifacts"
 	@echo "  clean-native     Remove Rust build artifacts"
@@ -196,6 +198,79 @@ chez-cross: $(SCHEME)
 	$(MAKE) -C $(CHEZ_CROSS_BUILD_DIR) install
 	@echo "==> [chez-cross] done. xpatch=$(CHEZ_XPATCH)  prefix=$(CHEZ_CROSS_PREFIX)"
 
+# ── Cross-compile the Rust native lib ────────────────────────────────────────
+# Maps a Chez machine type to a Rust target triple. Override RUST_TARGET to
+# point cargo at something else.
+#
+# Output: lib-cross/<machine>/libjerboa_native.{so,dylib}
+# Deploy: copy/symlink to lib/ on the target system before running the binary.
+RUST_TARGET_ta6le      = x86_64-unknown-linux-musl
+RUST_TARGET_tarm64le   = aarch64-unknown-linux-musl
+RUST_TARGET_ta6osx     = x86_64-apple-darwin
+RUST_TARGET_tarm64osx  = aarch64-apple-darwin
+RUST_TARGET_ta6fb      = x86_64-unknown-freebsd
+RUST_TARGET ?= $(RUST_TARGET_$(CHEZ_TARGET_MACHINE))
+
+# Feature set for cross builds. Many `full` features (duckdb, postgres,
+# mozjs) don't cross-compile cleanly; default to a minimal build.
+# Override e.g. CROSS_NATIVE_FEATURES="tls,crypto" to enable specific ones.
+CROSS_NATIVE_FEATURES ?=
+
+# Per-target Rust flags. musl targets enforce `+crt-static` by default which
+# disables cdylib — override so we can build .so. Other targets default empty.
+CROSS_RUSTFLAGS_x86_64-unknown-linux-musl  = -C target-feature=-crt-static
+CROSS_RUSTFLAGS_aarch64-unknown-linux-musl = -C target-feature=-crt-static
+CROSS_RUSTFLAGS ?= $(CROSS_RUSTFLAGS_$(RUST_TARGET))
+
+# Extension differs by target OS.
+CROSS_NATIVE_EXT_ta6le      = so
+CROSS_NATIVE_EXT_tarm64le   = so
+CROSS_NATIVE_EXT_ta6osx     = dylib
+CROSS_NATIVE_EXT_tarm64osx  = dylib
+CROSS_NATIVE_EXT_ta6fb      = so
+
+# Translate "x86_64-unknown-linux-musl" → "X86_64_UNKNOWN_LINUX_MUSL" for
+# cargo's CARGO_TARGET_<TRIPLE>_LINKER and AR_<triple> / CC_<triple> env vars.
+empty :=
+space := $(empty) $(empty)
+cargo_env_target = $(subst -,_,$(subst $(space),_,$(shell echo $(RUST_TARGET) | tr a-z A-Z)))
+cc_env_target = $(subst -,_,$(RUST_TARGET))
+
+# `rustup which` returns the absolute path inside the active rustup toolchain,
+# bypassing PATH. Needed because a side-by-side Homebrew `rust` install may
+# shadow rustup's cargo/rustc on $PATH — and Homebrew rust's rustlib only
+# contains the host target, so cross builds fail with E0463 ("can't find crate
+# for core") for any target other than aarch64-apple-darwin.
+RUSTUP_CARGO = $(shell rustup which cargo 2>/dev/null)
+RUSTUP_RUSTC = $(shell rustup which rustc 2>/dev/null)
+
+.PHONY: native-cross
+native-cross:
+	@test -n "$(CHEZ_TARGET_MACHINE)" \
+		|| { echo "ERROR: set CHEZ_TARGET_MACHINE=<machine-type>" >&2; exit 1; }
+	@test -n "$(RUST_TARGET)" \
+		|| { echo "ERROR: no RUST_TARGET mapping for $(CHEZ_TARGET_MACHINE); set RUST_TARGET=<triple>" >&2; exit 1; }
+	@test -n "$(CROSS_CC)" \
+		|| { echo "ERROR: set CROSS_CC=<cross-cc>" >&2; exit 1; }
+	@test -n "$(RUSTUP_CARGO)" \
+		|| { echo "ERROR: rustup not found (or no active toolchain); install via https://rustup.rs" >&2; exit 1; }
+	@echo "==> [native-cross] $(CHEZ_TARGET_MACHINE) (rust target: $(RUST_TARGET))"
+	@rustup target list --installed 2>/dev/null | grep -q "^$(RUST_TARGET)$$" \
+		|| rustup target add $(RUST_TARGET)
+	cd $(RUST_NATIVE_DIR) && \
+	  CARGO_TARGET_$(cargo_env_target)_LINKER="$(CROSS_CC)" \
+	  CC_$(cc_env_target)="$(CROSS_CC)" \
+	  AR_$(cc_env_target)="$(CROSS_AR)" \
+	  RUSTC="$(RUSTUP_RUSTC)" \
+	  $(if $(CROSS_RUSTFLAGS),RUSTFLAGS="$(CROSS_RUSTFLAGS)") \
+	  "$(RUSTUP_CARGO)" build --release --target=$(RUST_TARGET) \
+	    --no-default-features \
+	    $(if $(CROSS_NATIVE_FEATURES),--features $(CROSS_NATIVE_FEATURES))
+	@mkdir -p lib-cross/$(CHEZ_TARGET_MACHINE)
+	cp $(RUST_NATIVE_DIR)/target/$(RUST_TARGET)/release/libjerboa_native.$(CROSS_NATIVE_EXT_$(CHEZ_TARGET_MACHINE)) \
+	   lib-cross/$(CHEZ_TARGET_MACHINE)/
+	@echo "==> [native-cross] -> lib-cross/$(CHEZ_TARGET_MACHINE)/libjerboa_native.$(CROSS_NATIVE_EXT_$(CHEZ_TARGET_MACHINE))"
+
 # Cross-compiled binary. Drives support/build-binary.sh with TARGET_* env.
 .PHONY: binary-cross
 binary-cross: chez build chez-cross
diff --git a/docs/cross-compile.md b/docs/cross-compile.md
index a8698ce..6069e68 100644
--- a/docs/cross-compile.md
+++ b/docs/cross-compile.md
@@ -42,6 +42,23 @@ brew install musl-cross --with-aarch64
 #   /opt/homebrew/bin/aarch64-linux-musl-gcc
 ```
 
+For FreeBSD targets, Apple's clang plus a FreeBSD sysroot suffices. The
+repo ships `support/cross-cc-freebsd-{amd64,arm64}` — wrappers that point
+Apple's `/usr/bin/clang` at `.freebsd-sysroot/<arch>/` with the correct
+`--target=` triple. One-time setup:
+
+```sh
+brew install lld   # Apple's clang doesn't bundle ld.lld
+mkdir -p .freebsd-sysroot/amd64
+curl -L -o .freebsd-sysroot/base.txz \
+    https://download.freebsd.org/releases/amd64/14.4-RELEASE/base.txz
+tar -xf .freebsd-sysroot/base.txz -C .freebsd-sysroot/amd64 \
+    ./usr/include ./usr/lib ./lib
+```
+
+(Repeat with `arch=arm64` for `tarm64fb`. The sysroot dir is ~450 MB and is
+gitignored — it lives outside the repo's tracked tree.)
+
 ---
 
 ## End-to-end example
@@ -63,6 +80,48 @@ jerboa-bin-ta6le: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
                   statically linked, with debug_info, not stripped
 ```
 
+FreeBSD x86_64 from macOS arm64 (after sysroot setup, see above):
+
+```sh
+make binary-cross \
+    CHEZ_TARGET_MACHINE=ta6fb \
+    CROSS_CC=$PWD/support/cross-cc-freebsd-amd64 \
+    CROSS_AR=/usr/bin/ar \
+    CROSS_RANLIB=/usr/bin/ranlib
+```
+
+Output: `./jerboa-bin-ta6fb` — a dynamically-linked FreeBSD ELF
+(interpreter `/libexec/ld-elf.so.1`).
+
+```
+$ file jerboa-bin-ta6fb
+jerboa-bin-ta6fb: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD),
+                  dynamically linked, for FreeBSD 14.4, FreeBSD-style
+```
+
+### Rust native lib
+
+The Rust FFI lib (`jerboa-native-rs`) cross-compiles via `rustup` targets:
+
+```sh
+make native-cross CHEZ_TARGET_MACHINE=ta6le    CROSS_CC=x86_64-linux-musl-gcc  CROSS_AR=x86_64-linux-musl-ar
+make native-cross CHEZ_TARGET_MACHINE=tarm64le CROSS_CC=aarch64-linux-musl-gcc CROSS_AR=aarch64-linux-musl-ar
+```
+
+Output: `lib-cross/<machine>/libjerboa_native.{so,dylib}`. Deploy alongside
+the binary on the target (copy to `lib/` next to `jerboa-bin-<target>` or
+set `LD_LIBRARY_PATH`).
+
+> ⚠️  Musl targets enforce `+crt-static` by default, which disables
+> `cdylib`. The Makefile passes `-C target-feature=-crt-static` via
+> `RUSTFLAGS` for musl targets so the `.so` is produced.
+
+> ⚠️  If a side-by-side Homebrew `rust` is installed, it shadows rustup's
+> `cargo`/`rustc` on `$PATH` and only knows about the host target —
+> producing `E0463 "can't find crate for core"`. The Makefile bypasses
+> `$PATH` by invoking `rustup which cargo`/`rustup which rustc` absolute
+> paths.
+
 ---
 
 ## How it works
diff --git a/jerboa-native-rs/src/landlock.rs b/jerboa-native-rs/src/landlock.rs
index 9458a6e..f70b09c 100644
--- a/jerboa-native-rs/src/landlock.rs
+++ b/jerboa-native-rs/src/landlock.rs
@@ -3,12 +3,12 @@ use crate::panic::{ffi_wrap, set_last_error};
 // Landlock LSM syscall wrappers via libc
 // ABI v1-v5 support
 
-// Syscall numbers for x86_64
-#[cfg(target_arch = "x86_64")]
+// Syscall numbers for x86_64 and aarch64 (same on both per Linux kernel ABI).
+#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
 const SYS_LANDLOCK_CREATE_RULESET: libc::c_long = 444;
-#[cfg(target_arch = "x86_64")]
+#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
 const SYS_LANDLOCK_ADD_RULE: libc::c_long = 445;
-#[cfg(target_arch = "x86_64")]
+#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
 const SYS_LANDLOCK_RESTRICT_SELF: libc::c_long = 446;
 
 // ABI versions and access rights
diff --git a/support/cross-cc-freebsd b/support/cross-cc-freebsd
new file mode 100755
index 0000000..7484c20
--- /dev/null
+++ b/support/cross-cc-freebsd
@@ -0,0 +1,47 @@
+#!/bin/sh
+# cross-cc-freebsd — Apple clang wrapper that emits FreeBSD ELF.
+#
+# Usage:  cross-cc-freebsd <arch> [clang args...]
+#
+# <arch> is the leading argv[0] sniff value: "x86_64" or "aarch64".
+# The wrapper picks a target triple, points clang at the FreeBSD sysroot in
+# .freebsd-sysroot/<arch>/, and prepends the matching crt object dir to the
+# link line so the FreeBSD libc resolves correctly.
+#
+# Intended to be invoked through one of two argv[0] symlinks:
+#   support/cross-cc-freebsd-amd64    -> cross-cc-freebsd
+#   support/cross-cc-freebsd-arm64    -> cross-cc-freebsd
+#
+# Override CLANG=/path/to/clang if Apple's /usr/bin/clang is missing.
+
+set -eu
+
+CLANG="${CLANG:-/usr/bin/clang}"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+self="$(basename "$0")"
+case "$self" in
+    cross-cc-freebsd-amd64) arch=amd64;     triple=x86_64-unknown-freebsd14.0  ;;
+    cross-cc-freebsd-arm64) arch=arm64;     triple=aarch64-unknown-freebsd14.0 ;;
+    *)
+        echo "ERROR: cross-cc-freebsd must be invoked via cross-cc-freebsd-{amd64,arm64} symlink" >&2
+        exit 1
+        ;;
+esac
+
+SYSROOT="${FREEBSD_SYSROOT:-$REPO_DIR/.freebsd-sysroot/$arch}"
+[ -d "$SYSROOT/usr/include" ] || {
+    echo "ERROR: FreeBSD sysroot missing at $SYSROOT (expected usr/include + usr/lib)" >&2
+    echo "       Download with: curl -L -o .freebsd-sysroot/base.txz \\" >&2
+    echo "         https://download.freebsd.org/releases/$arch/14.4-RELEASE/base.txz" >&2
+    echo "       Then: tar -xf .freebsd-sysroot/base.txz -C .freebsd-sysroot/$arch ./usr/include ./usr/lib ./lib" >&2
+    exit 1
+}
+
+exec "$CLANG" \
+    --target="$triple" \
+    --sysroot="$SYSROOT" \
+    -B"$SYSROOT/usr/lib" \
+    -fuse-ld=lld \
+    "$@"
diff --git a/support/cross-cc-freebsd-amd64 b/support/cross-cc-freebsd-amd64
new file mode 120000
index 0000000..708e263
--- /dev/null
+++ b/support/cross-cc-freebsd-amd64
@@ -0,0 +1 @@
+cross-cc-freebsd
\ No newline at end of file
diff --git a/support/cross-cc-freebsd-arm64 b/support/cross-cc-freebsd-arm64
new file mode 120000
index 0000000..708e263
--- /dev/null
+++ b/support/cross-cc-freebsd-arm64
@@ -0,0 +1 @@
+cross-cc-freebsd
\ No newline at end of file