JERBOA_HOME := $(CURDIR)
PROJECT_VERSION ?= $(shell v=$$(tr -d '[:space:]' < VERSION 2>/dev/null || true); printf '%s\n' "$${v:-0.2.0}")
HOST_UNAME_S := $(shell uname -s)
HOST_UNAME_M := $(shell uname -m)
CHEZ_BUILD_DIR ?= $(JERBOA_HOME)/build/chez
CHEZ_PREFIX ?= $(JERBOA_HOME)/.chez
SCHEME ?= $(CHEZ_PREFIX)/bin/scheme

# Detect host machine type from the vendored configure script (e.g. tarm64osx
# on macOS arm64, ta6le on x86_64 linux). Used as XM= when bootstrapping
# native boot files via pb. Pulled out into a helper script because the sed
# pattern contains parens that confuse Make's $(shell ...) scanner.
CHEZ_MACHINE_TYPE := $(shell $(JERBOA_HOME)/support/detect-chez-machine.sh)

# `--enable-harden` makes Chez's configure add ELF-only hardening linker flags
# (-Wl,-z,relro,-z,now) to the bundled zlib/lz4 sub-builds. Apple's Mach-O linker
# rejects them ("ld: unknown options: -z"), breaking `make binary` on macOS. Gate
# it to ELF platforms (Linux, FreeBSD); macOS relies on its own RO/ASLR defaults.
CHEZ_UNAME_S := $(shell uname -s)
ifeq ($(CHEZ_UNAME_S),Darwin)
CHEZ_HARDEN ?=
else
CHEZ_HARDEN ?= --enable-harden
endif

CHEZ_INSTALL_FLAGS = \
		$(CHEZ_HARDEN) \
		--installprefix=$(CHEZ_PREFIX) \
		--installbin=$(CHEZ_PREFIX)/bin \
		--installlib=$(CHEZ_PREFIX)/lib \
	--installman=$(CHEZ_PREFIX)/share/man \
	--installdoc=$(CHEZ_PREFIX)/share/doc \
	--as-is
LIBDIRS = lib
# Base directory for chez-* repos (legacy C FFI — see `make native` for Rust backend)
JERBOA_EXT_DIR ?= $(HOME)/src
# External chez-* library paths for legacy wrapper modules
JERBOA_EXT_LIBDIRS = $(JERBOA_EXT_DIR)/jerboa-https/src:$(JERBOA_EXT_DIR)/jerboa-ssl/src:$(JERBOA_EXT_DIR)/chez-zlib/src:$(JERBOA_EXT_DIR)/jerboa-pcre2:$(JERBOA_EXT_DIR)/chez-leveldb:$(JERBOA_EXT_DIR)/chez-epoll/src:$(JERBOA_EXT_DIR)/jerboa-inotify/src:$(JERBOA_EXT_DIR)/jerboa-crypto/src:$(JERBOA_EXT_DIR)/jerboa-sqlite/src:$(JERBOA_EXT_DIR)/chez-postgresql/src
# Shared object paths for legacy FFI-based chez-* libraries
JERBOA_EXT_LDPATH = $(JERBOA_EXT_DIR)/jerboa-ssl:$(JERBOA_EXT_DIR)/chez-zlib:$(JERBOA_EXT_DIR)/jerboa-pcre2:$(JERBOA_EXT_DIR)/chez-leveldb:$(JERBOA_EXT_DIR)/chez-epoll:$(JERBOA_EXT_DIR)/jerboa-inotify:$(JERBOA_EXT_DIR)/jerboa-crypto:$(JERBOA_EXT_DIR)/jerboa-sqlite:$(JERBOA_EXT_DIR)/chez-postgresql
PURE_AUDIT_ROOT ?= $(HOME)/mine
PURE_AUDIT_ARGS ?= --summary --discover $(PURE_AUDIT_ROOT)
TYPED_SOURCES ?= tests/fixtures/typed/valid-split-tree.ss
TYPED_RUST_SOURCES ?= $(TYPED_SOURCES)
TYPED_RUST_DIR ?= build/typed/rust
TYPED_LLVMIR_SOURCES ?= tests/fixtures/typed/llvmir-basic.ss tests/fixtures/typed/llvmir-if.ss tests/fixtures/typed/llvmir-call.ss tests/fixtures/typed/llvmir-float.ss tests/fixtures/typed/llvmir-bitwise.ss tests/fixtures/typed/llvmir-for-fold.ss tests/fixtures/typed/llvmir-bytes.ss tests/fixtures/typed/llvmir-record.ss tests/fixtures/typed/llvmir-option.ss tests/fixtures/typed/llvmir-variant.ss tests/fixtures/typed/llvmir-smoke.ss
TYPED_LLVMIR_DIR ?= build/typed/llvmir
TYPED_LLVMIR_SMOKE_MODULE ?= sample_typed_llvmir_smoke
TYPED_LLVMIR_SMOKE_EXPECT ?= 42
TYPED_LLVMIR_PARITY_DIR ?= build/typed/llvmir-parity
TYPED_LLVMIR_PARITY_SOURCE ?= tests/fixtures/typed/llvmir-smoke.ss
TYPED_LLVMIR_PARITY_FN ?= sample_typed_llvmir_smoke::main
# LLVM tool discovery for the typed LLVM IR backend: PATH first, then the
# Homebrew LLVM keg (Apple Silicon, then Intel). Empty when no LLVM exists.
LLVM_BIN ?= $(shell if command -v llvm-as >/dev/null 2>&1; then dirname "$$(command -v llvm-as)"; elif [ -x /opt/homebrew/opt/llvm/bin/llvm-as ]; then echo /opt/homebrew/opt/llvm/bin; elif [ -x /usr/local/opt/llvm/bin/llvm-as ]; then echo /usr/local/opt/llvm/bin; fi)
TYPED_WRAPPER_DIR ?= build/typed/jerboa

.PHONY: help chez chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-llvmir typed-llvmir-check typed-llvmir-smoke typed-llvmir-parity typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-limits-primitives test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-llvmir test-typed-wrappers test-pure-audit 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-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-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>"
	@echo ""
	@echo "Build:"
	@echo "  chez             Build and install vendored Chez Scheme locally"
	@echo "  build            Compile all Jerboa libraries"
	@echo "  binary           Build a self-contained jerboa-bin binary (FreeBSD/Linux/macOS)"
	@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 "  jerboa           Build the busybox-style multicall binary: dist/jerboa"
	@echo "                   + symlinks dist/{jmcp,jlsp,jerbuild,jpkg} (one image, all five modes)"
	@echo "  system-check     Validate .jerboa-system manifest"
	@echo "  system-strict    Validate manifest and require declared outputs"
	@echo "  system-report    Print unification health/provenance report"
	@echo "  system-provenance Emit machine-readable provenance S-expression"
	@echo "  system-diff      Compare two manifests: OLD=path NEW=path"
	@echo "  audit-event-check Validate audit events: AUDIT=path"
	@echo "  generated-policy-check Validate generated artifact ignore policy"
	@echo "  workspace-health Summarize repo health: ROOT=path"
	@echo "  capability-plan Emit machine-readable capability plan"
	@echo "  capability-use-check Validate source capability use is declared"
	@echo "  native-boundary-check Validate native Rust/C/FFI boundaries"
	@echo "  install-policy-check Enforce no install-time code policy"
	@echo "  tcb-report       Emit machine-readable TCB inventory"
	@echo "  unification-check Run manifest/report/provenance/diff/audit checks"
	@echo "  jerboa-portable  Build multicall release targets: macos-arm64, linux-amd64, linux-arm64, freebsd-amd64"
	@echo "                   (cross targets need: make chez-cross CHEZ_TARGET_MACHINE=ta6le|tarm64le|ta6fb ...)"
	@echo "  release-artifact Package one built target: RELEASE_VERSION=v$(PROJECT_VERSION) RELEASE_TARGET=linux-amd64"
	@echo "  release-artifacts Build/package all release targets"
	@echo "  release-upload   Upload dist/release artifacts to a SourceHut git tag via hut"
	@echo "  native-cross     Cross-build Rust native lib for a target"
	@echo "                   Args: CHEZ_TARGET_MACHINE=<mt> CROSS_CC=<cc> [CROSS_NATIVE_FEATURES=tls,...]"
	@echo "  typecheck        Typecheck Typed Jerboa sources"
	@echo "  typed-rust       Generate Rust crate sources from Typed Jerboa"
	@echo "  typed-llvmir     Generate textual LLVM IR (.ll) from Typed Jerboa"
	@echo "  typed-llvmir-check Verify generated .ll with llvm-as + opt"
	@echo "  typed-llvmir-smoke Build and run an LLVM-backend smoke executable"
	@echo "  typed-llvmir-parity Compare LLVM and Rust backend observed results"
	@echo "  typed-wrappers   Generate Jerboa wrappers for typed Rust exports"
	@echo "  typed-build      Generate Rust crate sources/wrappers and run cargo build"
	@echo "  typed-wrapper-smoke Build and call generated typed wrappers through FFI"
	@echo "  typed-split-tree-smoke Build and call the typed split-tree fixture"
	@echo "  typed-test       Run Typed Jerboa front-end tests"
	@echo "  typed-clean      Remove generated typed build artifacts"
	@echo "  native           Build Rust native library"
	@echo "  clean            Remove compiled .so and .wpo artifacts"
	@echo "  clean-native     Remove Rust build artifacts"
	@echo "  audit-native     Run cargo audit on Rust native library"
	@echo "  pure-audit       Scan jerboa-* repos for non-pure runtime surfaces"
	@echo ""
	@echo "Cross-platform binaries (one static jerboa + jerbuild/jmcp/jlsp symlinks):"
	@echo "  jerboa-macos-arm64    Build for macOS arm64                 -> dist/macos-arm64/"
	@echo "  jerboa-linux-amd64    Cross-build for Linux x86_64 (musl)   -> dist/linux-amd64/"
	@echo "  jerboa-linux-arm64    Cross-build for Linux arm64 (musl)     -> dist/linux-arm64/"
	@echo "  jerboa-freebsd-amd64  Cross-build for FreeBSD amd64          -> dist/freebsd-amd64/"
	@echo "  jerboa-cross          Generic: CHEZ_TARGET_MACHINE=<mt> CROSS_CC=<cc>"
	@echo "                        (e.g. tarm64le -> dist/linux-arm64/, ta6fb -> freebsd-amd64)"
	@echo "  jerbuild-cross        Cross-build standalone jerbuild-<mt>  (same args)"
	@echo "  jerboa-portable       macos-arm64 + linux-amd64 + linux-arm64 + freebsd-amd64 in one shot"
	@echo "  NOTE one-time per target: build the cross Chez first, e.g."
	@echo "    make chez-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc   # Linux amd64"
	@echo ""
	@echo "MCP server (jerboa-mcp + jmcp binary):"
	@echo "  mcp              Compile-check the MCP server (mcp/server.ss)"
	@echo "  mcp-run          Run the MCP server from source over stdio"
	@echo "  mcp-test         Run the MCP protocol test suite from source"
	@echo "  jmcp             Build self-contained ./jmcp (embeds data/*.sexp)"
	@echo "  jmcp-portable    Build jmcp for {macOS,Linux,FreeBSD} x {amd64,arm64}"
	@echo "  mcp-test-binary  Run protocol tests against the built ./jmcp"
	@echo ""
	@echo "LSP server (jerboa-lsp + jlsp binary):"
	@echo "  lsp              Compile-check the LSP server (lsp/main.ss)"
	@echo "  api-signatures   Regenerate data/api-signatures.sexp from source"
	@echo "  lsp-gen          Regenerate completion tables from data/api-signatures.sexp"
	@echo "  lsp-run          Run the LSP server from source over stdio"
	@echo "  lsp-test         Run LSP unit + e2e tests"
	@echo "  jlsp             Build self-contained ./jlsp (installs as ~/.local/bin/jerboa-lsp)"
	@echo "  jlsp-install     Build ./jlsp and copy it to ~/.local/bin/jerboa-lsp"
	@echo "  jlsp-portable    Build jlsp for host + Linux/FreeBSD amd64"
	@echo ""
	@echo "Test (core):"
	@echo "  test             Run core test suite (reader, stdlib, ffi, regex)"
	@echo "  test-reader      Reader tests"
	@echo "  test-core        Core language tests"
	@echo "  test-runtime     Runtime tests"
	@echo "  test-stdlib      Standard library tests"
	@echo "  test-ffi         FFI tests"
	@echo "  test-modules     Module system tests"
	@echo "  test-expanded    Expanded stdlib tests"
	@echo "  test-regex-all   All regex tests (rawstring, regex, rx, peg)"
	@echo "  test-rawstring   Raw string reader tests"
	@echo "  test-regex       Regex tests"
	@echo "  test-rx          Rx pattern tests"
	@echo "  test-peg         PEG grammar tests"
	@echo "  test-repl        REPL tests"
	@echo "  test-functional  Functional tests (I/O, fork, signals)"
	@echo "  test-gaps        Gap coverage tests"
	@echo "  test-contract    Runtime contract tests"
	@echo "  test-ergo        Ergonomic contract marker tests"
	@echo "  test-limits-primitives Limits/sandbox/supervisor/audit primitive tests"
	@echo "  test-typed-parser Typed Jerboa parser tests"
	@echo "  test-typed-checker Typed Jerboa checker tests"
	@echo "  test-typed-rust Typed Jerboa Rust emitter tests"
	@echo "  test-typed-llvmir Typed Jerboa LLVM IR emitter tests"
	@echo "  test-typed-wrappers Typed Jerboa wrapper generator tests"
	@echo "  test-pure-audit  Pure Jerboa migration scanner tests"
	@echo ""
	@echo "Test (features):"
	@echo "  test-features    Phase 2 + Phase 3 feature tests"
	@echo "  test-wrappers    FFI wrapper module tests"
	@echo "  test-security    Security tests"
	@echo "  security         Run profile-aware local security checks"
	@echo "  test-native      Rust native library tests"
	@echo "  test-all         All test suites combined"
	@echo "  test-phase2      Phase 2 feature tests"
	@echo "  test-phase3      Phase 3 feature tests"
	@echo "  test-phase4a     Phase 4a: Core runtime tests"
	@echo "  test-phase4b     Phase 4b: Type system and safety tests"
	@echo "  test-phase4c     Phase 4c: Systems and performance tests"
	@echo "  test-phase4d     Phase 4d: Developer experience tests"
	@echo "  test-phase4e     Phase 4e: Data and distribution tests"
	@echo "  test-phase4f     Phase 4f: Toolchain and interop tests"
	@echo "  test-phase5      Phase 5: All sub-phases"
	@echo "  test-phase5e     Phase 5e: Systems and zero-cost tests"
	@echo "  test-phase6      Phase 6: Real programs tests"
	@echo "  test-phase7      Phase 7: Gerbil porting features"
	@echo "  test-phase8      Phase 8: Deep Gerbil compatibility"
	@echo ""
	@echo "Fuzzing:"
	@echo "  fuzz             Run all fuzz harnesses (default iterations)"
	@echo "  fuzz-smoke       Quick smoke fuzz for CI (~30s)"
	@echo "  fuzz-deep        Long-running deep fuzz (nightly)"
	@echo "  fuzz-<target>    Individual fuzz targets (reader, json, http2, ...)"
	@echo ""
	@echo "Docker:"
	@echo "  docker           Alias for docker-build"
	@echo "  docker-build     Build jerboa21/jerboa base image"
	@echo "  docker-push      Push base image to Docker Hub"

chez: $(SCHEME)

# Two-phase build:
#   1. Configure with --pb and run `make bootquick XM=$(CHEZ_MACHINE_TYPE)` so
#      the committed pb boot files compile native boot files for the host.
#   2. Reconfigure for the native machine type, then `make && make install`
#      builds the native kernel + scheme using the freshly-bootstrapped
#      boot files, and installs to $(CHEZ_PREFIX) (./.chez by default).
$(SCHEME): vendor/ChezScheme/configure
	@test -n "$(CHEZ_MACHINE_TYPE)" \
		|| { echo "ERROR: could not detect host machine type from configure --help" >&2; exit 1; }
	@mkdir -p $(CHEZ_BUILD_DIR)
	cd $(CHEZ_BUILD_DIR) && $(JERBOA_HOME)/vendor/ChezScheme/configure --pb --as-is
	$(MAKE) -C $(CHEZ_BUILD_DIR) bootquick XM=$(CHEZ_MACHINE_TYPE)
	cd $(CHEZ_BUILD_DIR) && $(JERBOA_HOME)/vendor/ChezScheme/configure $(CHEZ_INSTALL_FLAGS)
	$(MAKE) -C $(CHEZ_BUILD_DIR)
	$(MAKE) -C $(CHEZ_BUILD_DIR) install
	test -x $(SCHEME)

build: chez
	$(SCHEME) --libdirs $(LIBDIRS) --script support/build.ss

# Build a self-contained Jerboa binary that bundles petite.boot, scheme.boot,
# and a WPO-compiled entry program. Output: ./jerboa-bin
# Override entry script with BINARY_ENTRY=path/to/script.ss
BINARY_ENTRY ?= support/binary-entry.ss
BINARY_OUTPUT ?= jerboa-bin
binary: chez build
	SCHEME=$(SCHEME) JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) support/build-binary.sh $(BINARY_ENTRY) $(BINARY_OUTPUT)

# ── jerbuild standalone binary ───────────────────────────────────────────────
# Builds a self-contained ./jerbuild that bundles Chez + the Jerboa stdlib
# (lib/jerboa + lib/std). On any box with cargo/rustc/cc, this single binary
# is enough to transpile any Jerboa project's .ss sources to .sls.
#
#   ./jerbuild --jerboa-home     # extract bundled lib/, print path
#   ./jerbuild <src> <lib>       # transpile
.PHONY: jerbuild jerbuild-smoke
jerbuild: chez build
	SCHEME=$(SCHEME) JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) support/build-jerbuild.sh

# End-to-end smoke test: with no JERBOA_HOME and only ./jerbuild, transpile a
# small (export greet) module, then exec a script that imports + calls it.
# Catches regressions in transpile + bundled stdlib + exec subcommand.
jerbuild-smoke: jerbuild
	@SMOKE=$$(mktemp -d /tmp/jerbuild-smoke.XXXX); \
	mkdir -p $$SMOKE/in/demo; \
	printf '(export greet)\n(def (greet n) (format "hello, ~a!" n))\n' \
	    > $$SMOKE/in/demo/hello.ss; \
	printf '(import (demo hello)) (import (chezscheme))\n(display (greet "jerbuild")) (newline)\n' \
	    > $$SMOKE/main.ss; \
	cd $$SMOKE && env -u JERBOA_HOME $(CURDIR)/jerbuild in out >/dev/null; \
	JH=$$(env -u JERBOA_HOME $(CURDIR)/jerbuild --jerboa-home); \
	out=$$(cd $$SMOKE && env -u JERBOA_HOME $(CURDIR)/jerbuild exec --libdirs out:$$JH/lib main.ss 2>&1); \
	rm -rf $$SMOKE; \
	if [ "$$out" = "hello, jerbuild!" ]; then \
	    echo "jerbuild-smoke: PASS — $$out"; \
	else \
	    echo "jerbuild-smoke: FAIL — expected 'hello, jerbuild!', got: $$out" >&2; \
	    exit 1; \
	fi

# ── jerboa multicall binary (busybox-style) ──────────────────────────────────
# One binary; behavior selected by basename(argv[0]). Outputs dist/jerboa plus
# relative symlinks dist/{jmcp,jlsp,jerbuild}. Bundles Chez + the Jerboa stdlib
# so it can transpile/build a full Jerboa project on a bare host.
#
#   dist/jerboa <script.ss>      # REPL / run a script     (also: jerboa <mode>)
#   dist/jmcp                    # MCP server  (mcp/server.ss)
#   dist/jlsp                    # LSP server  (lsp/main-binary.ss)
#   dist/jerbuild <src> <lib>    # transpile;  dist/jerbuild binary E.ss OUT
.PHONY: jerboa jerboa-smoke
jerboa: chez build mcp-check lsp-gen
	$(SCHEME) --script support/build-jerboa-multicall.ss

.PHONY: system-check
system-check:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-system-manifest.ss .jerboa-system

.PHONY: system-strict
system-strict:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-system-manifest.ss --strict .jerboa-system

.PHONY: system-report
system-report:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/system-report.ss .jerboa-system

.PHONY: system-provenance
system-provenance:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/system-provenance.ss .jerboa-system

OLD ?= .jerboa-system
NEW ?= .jerboa-system
.PHONY: system-diff
system-diff:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/system-diff.ss "$(OLD)" "$(NEW)"

AUDIT ?= examples/audit-events.sexp
.PHONY: audit-event-check
audit-event-check:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-audit-events.ss .jerboa-system "$(AUDIT)"

.PHONY: generated-policy-check
generated-policy-check:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-generated-policy.ss .jerboa-system .gitignore

ROOT ?= .
.PHONY: workspace-health
workspace-health:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/workspace-health.ss "$(ROOT)"

.PHONY: capability-plan
capability-plan:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/capability-plan.ss .jerboa-system

.PHONY: capability-use-check
capability-use-check:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-capability-use.ss .jerboa-system

.PHONY: native-boundary-check
native-boundary-check:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-native-boundary.ss .jerboa-system

.PHONY: install-policy-check
install-policy-check:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/check-install-policy.ss .jerboa-system

.PHONY: tcb-report
tcb-report:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tools/tcb-report.ss .jerboa-system

.PHONY: unification-check
unification-check: system-check generated-policy-check workspace-health capability-plan capability-use-check native-boundary-check install-policy-check system-report system-provenance tcb-report system-diff audit-event-check

.PHONY: unification-release-check
unification-release-check: system-strict generated-policy-check workspace-health capability-plan capability-use-check native-boundary-check install-policy-check system-report system-provenance tcb-report system-diff audit-event-check

# End-to-end smoke test: all four modes from the single binary + its symlinks.
jerboa-smoke: jerboa
	@D=$(CURDIR)/dist; fail=0; \
	echo '(import (jerboa prelude)) (displayln (+ 40 2))' > /tmp/jms.ss; \
	[ "$$($$D/jerboa /tmp/jms.ss)" = "42" ] && echo "  jerboa script: PASS" || { echo "  jerboa script: FAIL" >&2; fail=1; }; \
	$$D/jlsp --version >/dev/null 2>&1 && echo "  jlsp:          PASS" || { echo "  jlsp: FAIL" >&2; fail=1; }; \
	echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | $$D/jmcp 2>/dev/null | grep -q '"jerboa-mcp"' && echo "  jmcp:          PASS" || { echo "  jmcp: FAIL" >&2; fail=1; }; \
	$$D/jerbuild --version | grep -q 'sha256=' && echo "  jerbuild:      PASS" || { echo "  jerbuild: FAIL" >&2; fail=1; }; \
	$$D/jerboa jlsp --version >/dev/null 2>&1 && echo "  jerboa jlsp:   PASS" || { echo "  jerboa jlsp: FAIL" >&2; fail=1; }; \
	echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | $$D/jerboa jmcp 2>/dev/null | grep -q '"jerboa-mcp"' && echo "  jerboa jmcp:   PASS" || { echo "  jerboa jmcp: FAIL" >&2; fail=1; }; \
	$$D/jerboa jerbuild --version | grep -q 'sha256=' && echo "  jerboa build:  PASS" || { echo "  jerboa build: FAIL" >&2; fail=1; }; \
	rm -f /tmp/jms.ss; \
	[ $$fail -eq 0 ] && echo "jerboa-smoke: PASS" || { echo "jerboa-smoke: FAIL" >&2; exit 1; }

# Install the native multicall binary + jmcp/jlsp symlinks into PREFIX/bin.
# Defaults to ~/.local/bin; override e.g. `make install PREFIX=/usr/local`.
# Copies what `make jerboa` produced in dist/ (run that first).
PREFIX ?= $(HOME)/.local
BINDIR ?= $(PREFIX)/bin
.PHONY: install
install:
	@test -x "$(CURDIR)/dist/jerboa" || { echo "ERROR: dist/jerboa not built — run 'make jerboa' first" >&2; exit 1; }
	@mkdir -p "$(BINDIR)"
	@install -m 0755 "$(CURDIR)/dist/jerboa" "$(BINDIR)/jerboa"
	@# macOS: overwriting a Mach-O in place invalidates its (ad-hoc) code
	@# signature, so the kernel SIGKILLs it ("Killed: 9"). Re-sign ad-hoc.
	@case "$$(uname -s)" in Darwin) \
		codesign --force --sign - "$(BINDIR)/jerboa" >/dev/null 2>&1 \
		&& echo "Re-signed $(BINDIR)/jerboa (macOS Gatekeeper)" \
		|| echo "WARNING: codesign failed — binary may be killed by Gatekeeper" >&2 ;; \
	esac
	@ln -sf jerboa "$(BINDIR)/jmcp"
	@ln -sf jerboa "$(BINDIR)/jlsp"
	@ln -sf jerboa "$(BINDIR)/jerbuild"
	@ln -sf jerboa "$(BINDIR)/jpkg"
	@echo "Installed: $(BINDIR)/{jerboa,jmcp,jlsp,jerbuild,jpkg}"
	@case ":$$PATH:" in *":$(BINDIR):"*) ;; *) echo "NOTE: $(BINDIR) is not on PATH — add it to use these commands" >&2 ;; esac

# ── Cross-compiled multicall binary ──────────────────────────────────────────
# Builds dist/<os-arch>/{jerboa,jmcp,jlsp,jerbuild} for a foreign target, all
# four modes runnable on that target. The xpatch retargets the host Chez's
# codegen; the cross Chez supplies target boot files + libkernel.a.
#
# On-target `jerbuild binary` works from a cross binary. Chez cross-compilation
# emits host-tagged .wpo (only .so get the target tag), and target-tagged .wpo
# can only be produced natively ON the target — so jerbuild re-invokes itself in
# bare-Chez mode (`<self> scheme <helper>`) to recompile the bundled stdlib
# sources to native .wpo in a fresh process with an empty library table. The
# produced binary's link libs are recorded in the bundle (csv/<mt>/os-libs:
# static + -no-pie on linux) so a cross target's native cc links cleanly.
#
# Generic form (builds the cross Chez on demand via chez-cross):
#   make jerboa-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc
.PHONY: jerboa-cross jerboa-macos-arm64 jerboa-linux-amd64 jerboa-linux-arm64 jerboa-freebsd-amd64 jerboa-portable
jerboa-cross: chez build mcp-check lsp-gen chez-cross
	@test -n "$(CHEZ_TARGET_MACHINE)" \
		|| { echo "ERROR: set CHEZ_TARGET_MACHINE=<machine-type>" >&2; exit 1; }
	JERBOA_HOME=$(JERBOA_HOME) \
	TARGET_MACHINE=$(CHEZ_TARGET_MACHINE) \
	JERBOA_CROSS_PREFIX=$(CHEZ_CROSS_PREFIX) \
	JERBOA_XPATCH=$(CHEZ_XPATCH) \
	CC="$(CROSS_CC)" \
	$(SCHEME) --script support/build-jerboa-multicall.ss

# Convenience targets: build the cross Chez on demand (only if absent), then
# the multicall. Re-running is fast once `.chez-cross-<m>` exists.
jerboa-macos-arm64: jerboa
	@case "$$(uname -s)-$$(uname -m)" in \
		Darwin-arm64) ;; \
		*) echo "ERROR: macos-arm64 release artifact must be built on macOS arm64" >&2; exit 1 ;; \
	esac
	@mkdir -p dist/macos-arm64
	@cp -f dist/jerboa dist/macos-arm64/jerboa
	@chmod 0755 dist/macos-arm64/jerboa
	@for link in jmcp jlsp jerbuild jpkg; do ln -sf jerboa dist/macos-arm64/$$link; done
	@echo "=== Staged jerboa multicall: macos-arm64 -> dist/macos-arm64/ ==="

jerboa-linux-amd64: chez build mcp-check lsp-gen
	@command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { echo "ERROR: x86_64-linux-musl-gcc not on PATH" >&2; exit 1; }
	@if test ! -d "$(JERBOA_HOME)/.chez-cross-ta6le" || test ! -f "$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch"; then \
		echo "=== cross Chez (ta6le) missing — building it first ==="; \
		$(MAKE) chez-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc; \
	fi
	@echo "=== Cross-building jerboa multicall: linux-amd64 (ta6le) ==="
	@JERBOA_HOME=$(JERBOA_HOME) TARGET_MACHINE=ta6le \
		JERBOA_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6le \
		JERBOA_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch \
		CC=x86_64-linux-musl-gcc \
		$(SCHEME) --script support/build-jerboa-multicall.ss

jerboa-linux-arm64: chez build mcp-check lsp-gen
	@command -v aarch64-linux-musl-gcc >/dev/null 2>&1 || { echo "ERROR: aarch64-linux-musl-gcc not on PATH" >&2; exit 1; }
	@if test ! -d "$(JERBOA_HOME)/.chez-cross-tarm64le" || test ! -f "$(CHEZ_BUILD_DIR)/xc-tarm64le/s/xpatch"; then \
		echo "=== cross Chez (tarm64le) missing — building it first ==="; \
		$(MAKE) chez-cross CHEZ_TARGET_MACHINE=tarm64le CROSS_CC=aarch64-linux-musl-gcc; \
	fi
	@echo "=== Cross-building jerboa multicall: linux-arm64 (tarm64le) ==="
	@JERBOA_HOME=$(JERBOA_HOME) TARGET_MACHINE=tarm64le \
		JERBOA_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-tarm64le \
		JERBOA_XPATCH=$(CHEZ_BUILD_DIR)/xc-tarm64le/s/xpatch \
		CC=aarch64-linux-musl-gcc \
		$(SCHEME) --script support/build-jerboa-multicall.ss

jerboa-freebsd-amd64: chez build mcp-check lsp-gen
	@test -x "$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" || { echo "ERROR: support/cross-cc-freebsd-amd64 not found/executable" >&2; exit 1; }
	@if test ! -d "$(JERBOA_HOME)/.chez-cross-ta6fb" || test ! -f "$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch"; then \
		echo "=== cross Chez (ta6fb) missing — building it first ==="; \
		$(MAKE) chez-cross CHEZ_TARGET_MACHINE=ta6fb CROSS_CC="$(JERBOA_HOME)/support/cross-cc-freebsd-amd64"; \
	fi
	@echo "=== Cross-building jerboa multicall: freebsd-amd64 (ta6fb) ==="
	@JERBOA_HOME=$(JERBOA_HOME) TARGET_MACHINE=ta6fb \
		JERBOA_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6fb \
		JERBOA_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch \
		CC="$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" \
		$(SCHEME) --script support/build-jerboa-multicall.ss

# Release targets.
jerboa-portable: jerboa-macos-arm64 jerboa-linux-amd64 jerboa-linux-arm64 jerboa-freebsd-amd64

# ── Release packaging for SourceHut tag artifacts ───────────────────────────
# Native SourceHut jobs call `release-artifact` after `make jerboa`. Local
# maintainers can call `release-artifacts` to cross-build and package all
# supported bootstrap toolchains in one pass.
RELEASE_VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || printf 'v%s\n' "$(PROJECT_VERSION)")
RELEASE_TARGET ?= $(shell host="$(HOST_UNAME_S)-$(HOST_UNAME_M)"; if [ "$$host" = Darwin-arm64 ]; then echo macos-arm64; elif [ "$$host" = Linux-x86_64 ]; then echo linux-amd64; elif [ "$$host" = Linux-aarch64 ] || [ "$$host" = Linux-arm64 ]; then echo linux-arm64; elif [ "$$host" = FreeBSD-amd64 ] || [ "$$host" = FreeBSD-x86_64 ]; then echo freebsd-amd64; else echo unknown; fi)
RELEASE_TARGETS ?= macos-arm64 linux-amd64 linux-arm64 freebsd-amd64
RELEASE_DIR ?= dist/release
RELEASE_REPO ?= ~lisp/jerboa

.PHONY: release-artifact release-artifacts release-upload
release-artifact:
	@support/package-jerboa-release.sh "$(RELEASE_VERSION)" "$(RELEASE_TARGET)" "$(RELEASE_DIR)"

release-artifacts: jerboa-portable
	@for target in $(RELEASE_TARGETS); do \
		support/package-jerboa-release.sh "$(RELEASE_VERSION)" "$$target" "$(RELEASE_DIR)"; \
	done

release-upload:
	@support/sourcehut-release-upload.sh "$(RELEASE_VERSION)" "$(RELEASE_REPO)" \
		"$(RELEASE_DIR)"/jerboa-"$(RELEASE_VERSION)"-*.tar.gz \
		"$(RELEASE_DIR)"/jerboa-"$(RELEASE_VERSION)"-*.tar.gz.sha256

# Cross-compiled jerbuild. Drives support/build-jerbuild.sh with TARGET_* env,
# producing ./jerbuild-<machine> alongside the host ./jerbuild.
#
# Usage:
#   make jerbuild-cross CHEZ_TARGET_MACHINE=ta6le    CROSS_CC=x86_64-linux-musl-gcc
#   make jerbuild-cross CHEZ_TARGET_MACHINE=tarm64le CROSS_CC=aarch64-linux-musl-gcc
#   make jerbuild-cross CHEZ_TARGET_MACHINE=ta6fb    CROSS_CC=$$PWD/support/cross-cc-freebsd-amd64
.PHONY: jerbuild-cross
jerbuild-cross: chez build chez-cross
	@test -n "$(CHEZ_TARGET_MACHINE)" \
		|| { echo "ERROR: set CHEZ_TARGET_MACHINE=<machine-type>" >&2; exit 1; }
	SCHEME=$(SCHEME) \
	JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) \
	JERBOA_CROSS_PREFIX=$(CHEZ_CROSS_PREFIX) \
	JERBOA_XPATCH=$(CHEZ_XPATCH) \
	TARGET_MACHINE=$(CHEZ_TARGET_MACHINE) \
	CC="$(CROSS_CC)" \
	OUTPUT=jerbuild-$(CHEZ_TARGET_MACHINE) \
	support/build-jerbuild.sh

# ── Cross-compilation ────────────────────────────────────────────────────────
# Override these to cross-compile:
#   CHEZ_TARGET_MACHINE — Chez machine type (e.g. ta6osx, ta6le, tarm64le, ta6fb)
#   CROSS_CC            — cross C compiler (e.g. "clang -arch x86_64",
#                                              x86_64-linux-musl-gcc)
#   CROSS_CC_FOR_BUILD  — host-runnable cc used to build zuo (defaults to cc)
#   CROSS_AR / CROSS_RANLIB — target archiver/ranlib. Default: derived from CROSS_CC
#       prefix when CROSS_CC matches "<triple>-gcc" (e.g. x86_64-linux-musl-gcc
#       implies x86_64-linux-musl-ar / x86_64-linux-musl-ranlib).
#   CROSS_STRIP        — target strip (same derivation as AR; "true" to skip).
CHEZ_TARGET_MACHINE ?=
CROSS_CC ?=
CROSS_CC_FOR_BUILD ?= cc

# Derive AR/RANLIB/STRIP from CROSS_CC when it has a "<triple>-gcc" shape.
# Caller can still override explicitly.
CROSS_TOOL_PREFIX := $(patsubst %-gcc,%-,$(firstword $(CROSS_CC)))
CROSS_AR     ?= $(if $(filter %-,$(CROSS_TOOL_PREFIX)),$(CROSS_TOOL_PREFIX)ar,)
CROSS_RANLIB ?= $(if $(filter %-,$(CROSS_TOOL_PREFIX)),$(CROSS_TOOL_PREFIX)ranlib,)
CROSS_STRIP  ?= $(if $(filter %-,$(CROSS_TOOL_PREFIX)),$(CROSS_TOOL_PREFIX)strip,true)

CHEZ_CROSS_PREFIX = $(JERBOA_HOME)/.chez-cross-$(CHEZ_TARGET_MACHINE)
CHEZ_CROSS_BUILD_DIR = $(JERBOA_HOME)/build/chez-cross-$(CHEZ_TARGET_MACHINE)
CHEZ_XPATCH = $(CHEZ_BUILD_DIR)/xc-$(CHEZ_TARGET_MACHINE)/s/xpatch

# Extra flags for the cross Chez configure. Callers can add e.g. `--static`
# for fully-static musl builds. We default to disabling curses/x11/iconv
# because (a) self-contained binaries don't need a REPL or X11, and (b) the
# musl-cross sysroot on macOS typically lacks ncurses/x11 headers.
CROSS_CHEZ_CONFIGURE_FLAGS ?= --threads --enable-harden --disable-x11 --disable-curses --disable-iconv

CHEZ_CROSS_INSTALL_FLAGS = \
	--installprefix=$(CHEZ_CROSS_PREFIX) \
	--installbin=$(CHEZ_CROSS_PREFIX)/bin \
	--installlib=$(CHEZ_CROSS_PREFIX)/lib \
	--installman=$(CHEZ_CROSS_PREFIX)/share/man \
	--installdoc=$(CHEZ_CROSS_PREFIX)/share/doc \
	--as-is

# Build target boot files + xpatch (in host build dir) + target libkernel.a
# (in a separate cross build dir). Requires the host Chez to already be built.
.PHONY: chez-cross
chez-cross: $(SCHEME)
	@test -n "$(CHEZ_TARGET_MACHINE)" \
		|| { echo "ERROR: set CHEZ_TARGET_MACHINE=<machine-type> (e.g. ta6osx, ta6le)" >&2; exit 1; }
	@test -n "$(CROSS_CC)" \
		|| { echo "ERROR: set CROSS_CC=<cross-cc> (e.g. \"clang -arch x86_64\")" >&2; exit 1; }
	@echo "==> [chez-cross] bootquick XM=$(CHEZ_TARGET_MACHINE) (produces target boot files + xpatch)"
	$(MAKE) -C $(CHEZ_BUILD_DIR) bootquick XM=$(CHEZ_TARGET_MACHINE)
	@test -f $(CHEZ_XPATCH) || { echo "ERROR: xpatch not produced at $(CHEZ_XPATCH)" >&2; exit 1; }
	@echo "==> [chez-cross] configure --cross --force -m=$(CHEZ_TARGET_MACHINE)"
	@mkdir -p $(CHEZ_CROSS_BUILD_DIR)
	cd $(CHEZ_CROSS_BUILD_DIR) && $(JERBOA_HOME)/vendor/ChezScheme/configure \
		--cross --force -m=$(CHEZ_TARGET_MACHINE) \
		$(CROSS_CHEZ_CONFIGURE_FLAGS) \
		CC="$(CROSS_CC)" CC_FOR_BUILD="$(CROSS_CC_FOR_BUILD)" \
		$(if $(CROSS_AR),AR="$(CROSS_AR)") \
		$(if $(CROSS_RANLIB),RANLIB="$(CROSS_RANLIB)") \
		$(CHEZ_CROSS_INSTALL_FLAGS)
	@echo "==> [chez-cross] stage target boot files + generated headers into cross dir"
	@mkdir -p $(CHEZ_CROSS_BUILD_DIR)/boot/$(CHEZ_TARGET_MACHINE)
	@mkdir -p $(CHEZ_CROSS_BUILD_DIR)/$(CHEZ_TARGET_MACHINE)/boot/$(CHEZ_TARGET_MACHINE)
	cp $(CHEZ_BUILD_DIR)/boot/$(CHEZ_TARGET_MACHINE)/* \
	   $(CHEZ_CROSS_BUILD_DIR)/boot/$(CHEZ_TARGET_MACHINE)/
	cp $(CHEZ_BUILD_DIR)/boot/$(CHEZ_TARGET_MACHINE)/* \
	   $(CHEZ_CROSS_BUILD_DIR)/$(CHEZ_TARGET_MACHINE)/boot/$(CHEZ_TARGET_MACHINE)/
	@echo "==> [chez-cross] build target libkernel.a"
	@# CHOST tells the bundled zlib's configure to use the cross-prefix toolchain
	@# instead of running host-OS detection (which on macOS picks libtool, not ar).
	CHOST="$(CROSS_TOOL_PREFIX:%-=%)" \
	AR="$(CROSS_AR)" \
	RANLIB="$(CROSS_RANLIB)" \
	$(MAKE) -C $(CHEZ_CROSS_BUILD_DIR)
	$(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
	@test -n "$(CHEZ_TARGET_MACHINE)" \
		|| { echo "ERROR: set CHEZ_TARGET_MACHINE=<machine-type>" >&2; exit 1; }
	SCHEME=$(SCHEME) \
	JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) \
	JERBOA_CROSS_PREFIX=$(CHEZ_CROSS_PREFIX) \
	JERBOA_XPATCH=$(CHEZ_XPATCH) \
	TARGET_MACHINE=$(CHEZ_TARGET_MACHINE) \
	CC="$(CROSS_CC)" \
	support/build-binary.sh $(BINARY_ENTRY) $(BINARY_OUTPUT)-$(CHEZ_TARGET_MACHINE)

# ── MCP server (jerboa-mcp) + jmcp self-contained binary ─────────────────────
# mcp/server.ss is the pure-Jerboa MCP server; data/*.sexp (cookbooks,
# features, api-signatures, security rules, …) is its knowledge base. `make
# jmcp` embeds the current data/ into a self-contained ./jmcp, so the binary is
# always rebuilt from this repo — there is no separate MCP repo to update.
# Re-run `make jmcp` after data/ changes (e.g. new howto/feature entries) to
# ship the updated knowledge base.
MCP_ENTRY = mcp/server.ss
MCP_TEST  = mcp/test/protocol-test.ss
.PHONY: mcp mcp-run mcp-test mcp-test-binary mcp-check jmcp jmcp-portable \
        jmcp-macos-amd64 jmcp-linux-amd64 jmcp-linux-arm64 \
        jmcp-freebsd-amd64 jmcp-freebsd-arm64

# Source hygiene scoped to the MCP tree. Jerboa proper legitimately uses .sls
# and (chezscheme); only mcp/*.ss must stay pure Jerboa and data/ stays .sexp.
mcp-check:
	@test "$$(find mcp -name '*.sls' | wc -l | tr -d ' ')" = "0" \
		|| { echo "ERROR: .sls under mcp/ — server must stay pure Jerboa" >&2; exit 1; }
	@test "$$(find data -name '*.json' | wc -l | tr -d ' ')" = "0" \
		|| { echo "ERROR: .json in data/ — knowledge base must stay .sexp" >&2; exit 1; }
	@! grep -l "(chezscheme)" mcp/*.ss >/dev/null 2>&1 \
		|| { echo "ERROR: (chezscheme) imported in mcp/ source" >&2; exit 1; }

mcp: chez mcp-check
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_MCP_SCHEME_PATH=$(SCHEME) \
		$(SCHEME) --libdirs $(LIBDIRS) --script $(MCP_ENTRY) < /dev/null
	@echo "mcp: server compile-check OK"

mcp-run:
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_MCP_SCHEME_PATH=$(SCHEME) \
		$(SCHEME) --libdirs $(LIBDIRS) --script $(MCP_ENTRY)

mcp-test: chez mcp-check
	@JERBOA_MCP_MODE=full JERBOA_HOME=$(JERBOA_HOME) JERBOA_MCP_SCHEME_PATH=$(SCHEME) \
		$(SCHEME) --libdirs $(LIBDIRS) --script $(MCP_TEST)

mcp-test-binary: jmcp mcp-check
	@JERBOA_MCP_MODE=full JERBOA_MCP_SERVER=$(JERBOA_HOME)/jmcp \
		JERBOA_MCP_REPO=/tmp/jerboa-mcp-no-data \
		JERBOA_HOME=$(JERBOA_HOME) JERBOA_MCP_SCHEME_PATH=$(SCHEME) \
		$(SCHEME) --libdirs $(LIBDIRS) --script $(MCP_TEST)

# Native self-contained binary for the host (./jmcp). Embeds data/*.sexp.
jmcp: chez build mcp-check
	@echo "=== Building native ./jmcp (host) ==="
	@JERBOA_HOME=$(JERBOA_HOME) SCHEME=$(SCHEME) JMCP_OUTPUT=jmcp mcp/build-jmcp.sh

# Cross-built binaries reuse jerboa's own cross prefixes (.chez-cross-<m>) and
# xpatch (build/chez/xc-<m>/s/xpatch). The native host build (make jmcp) covers
# macOS arm64; the targets below cover the other five.
jmcp-macos-amd64: chez build mcp-check
	@test -d "$(JERBOA_HOME)/.chez-cross-ta6osx" || { echo "ERROR: .chez-cross-ta6osx not found (make chez-cross CHEZ_TARGET_MACHINE=ta6osx CROSS_CC='cc -arch x86_64')" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6osx/s/xpatch" || { echo "ERROR: ta6osx xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jmcp-macos-amd64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) SCHEME=$(SCHEME) CC="cc -arch x86_64" \
		JMCP_OUTPUT=jmcp-macos-amd64 JMCP_MACHINE=ta6osx JMCP_TARGET_OS=macos \
		JMCP_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6osx \
		JMCP_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6osx/s/xpatch \
		mcp/build-jmcp.sh

jmcp-linux-amd64: chez build mcp-check
	@command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { echo "ERROR: x86_64-linux-musl-gcc not on PATH" >&2; exit 1; }
	@test -d "$(JERBOA_HOME)/.chez-cross-ta6le" || { echo "ERROR: .chez-cross-ta6le not found (make chez-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc)" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch" || { echo "ERROR: ta6le xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jmcp-linux-amd64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) SCHEME=$(SCHEME) CC=x86_64-linux-musl-gcc \
		JMCP_OUTPUT=jmcp-linux-amd64 JMCP_MACHINE=ta6le JMCP_TARGET_OS=linux \
		JMCP_STATIC=1 JMCP_DLOPEN_STUBS=1 \
		JMCP_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6le \
		JMCP_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch \
		mcp/build-jmcp.sh

jmcp-linux-arm64: chez build mcp-check
	@command -v aarch64-linux-musl-gcc >/dev/null 2>&1 || { echo "ERROR: aarch64-linux-musl-gcc not on PATH" >&2; exit 1; }
	@test -d "$(JERBOA_HOME)/.chez-cross-tarm64le" || { echo "ERROR: .chez-cross-tarm64le not found (make chez-cross CHEZ_TARGET_MACHINE=tarm64le CROSS_CC=aarch64-linux-musl-gcc)" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-tarm64le/s/xpatch" || { echo "ERROR: tarm64le xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jmcp-linux-arm64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) SCHEME=$(SCHEME) CC=aarch64-linux-musl-gcc \
		JMCP_OUTPUT=jmcp-linux-arm64 JMCP_MACHINE=tarm64le JMCP_TARGET_OS=linux \
		JMCP_STATIC=1 JMCP_DLOPEN_STUBS=1 \
		JMCP_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-tarm64le \
		JMCP_XPATCH=$(CHEZ_BUILD_DIR)/xc-tarm64le/s/xpatch \
		mcp/build-jmcp.sh

jmcp-freebsd-amd64: chez build mcp-check
	@test -x "$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" || { echo "ERROR: support/cross-cc-freebsd-amd64 not found/executable" >&2; exit 1; }
	@test -d "$(JERBOA_HOME)/.chez-cross-ta6fb" || { echo "ERROR: .chez-cross-ta6fb not found (make chez-cross CHEZ_TARGET_MACHINE=ta6fb CROSS_CC=...)" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch" || { echo "ERROR: ta6fb xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jmcp-freebsd-amd64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) SCHEME=$(SCHEME) CC="$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" \
		JMCP_OUTPUT=jmcp-freebsd-amd64 JMCP_MACHINE=ta6fb JMCP_TARGET_OS=freebsd \
		JMCP_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-ta6fb \
		JMCP_XPATCH=$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch \
		mcp/build-jmcp.sh

jmcp-freebsd-arm64: chez build mcp-check
	@test -x "$(JERBOA_HOME)/support/cross-cc-freebsd-arm64" || { echo "ERROR: support/cross-cc-freebsd-arm64 not found/executable" >&2; exit 1; }
	@test -d "$(JERBOA_HOME)/.chez-cross-tarm64fb" || { echo "ERROR: .chez-cross-tarm64fb not found (make chez-cross CHEZ_TARGET_MACHINE=tarm64fb CROSS_CC=...)" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-tarm64fb/s/xpatch" || { echo "ERROR: tarm64fb xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jmcp-freebsd-arm64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) SCHEME=$(SCHEME) CC="$(JERBOA_HOME)/support/cross-cc-freebsd-arm64" \
		JMCP_OUTPUT=jmcp-freebsd-arm64 JMCP_MACHINE=tarm64fb JMCP_TARGET_OS=freebsd \
		JMCP_CROSS_PREFIX=$(JERBOA_HOME)/.chez-cross-tarm64fb \
		JMCP_XPATCH=$(CHEZ_BUILD_DIR)/xc-tarm64fb/s/xpatch \
		mcp/build-jmcp.sh

# All six: host (macOS arm64) + the five cross targets.
jmcp-portable: jmcp jmcp-macos-amd64 jmcp-linux-amd64 jmcp-linux-arm64 jmcp-freebsd-amd64 jmcp-freebsd-arm64

# ── LSP server (jerboa-lsp) + jlsp self-contained binary ─────────────────────
# lsp/ is the pure-Scheme Jerboa language server. Its completion tables
# (lsp/analysis/completion-tables.ss) are GENERATED by `make lsp-gen` from
# data/api-signatures.sexp — the same API map the MCP serves — so completions
# track the live standard library. Every LSP target regenerates them first, so
# the tables can never go stale; the file itself is not committed (.gitignore).
# The host binary is ./jlsp; it installs as ~/.local/bin/jerboa-lsp (the name
# LSP clients already expect), with all boot files + program embedded.
LSP_ENTRY     = lsp/main.ss
LSP_GEN       = lsp/gen-completion-tables.ss
API_SIG_GEN   = tools/gen-api-signatures.ss
LSP_UNIT_TEST = lsp/test/unit-test.ss
LSP_E2E_TEST  = lsp/test/e2e-test.py
LSP_BIN       = $(HOME)/.local/bin/jerboa-lsp
# Cross builds compile lsp/ + std/ against the host libdirs; boot files come
# from jerboa's own cross prefixes (.chez-cross-<m>).
LSP_XC_LIBDIRS = $(JERBOA_HOME):$(JERBOA_HOME)/lib:$(JERBOA_HOME)/.chez-cross-ta6le/lib:$(JERBOA_HOME)/.chez-cross-ta6fb/lib
.PHONY: api-signatures lsp lsp-gen lsp-run lsp-test lsp-test-unit lsp-test-e2e \
        jlsp jlsp-install jlsp-linux-amd64 jlsp-freebsd-amd64 jlsp-portable

# Regenerate the MCP/LSP API map from current library-form source files.
api-signatures: chez
	@JERBOA_HOME=$(JERBOA_HOME) $(SCHEME) --script $(API_SIG_GEN)

# Regenerate the completion tables from the live API map (data/api-signatures.sexp).
lsp-gen: chez
	@JERBOA_HOME=$(JERBOA_HOME) $(SCHEME) --script $(LSP_GEN)

# Compile-check the whole server from source (imports the full cascade).
lsp: chez lsp-gen
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_LSP_DIR=$(JERBOA_HOME) \
		$(SCHEME) -q --script $(LSP_ENTRY) -- --version
	@echo "lsp: server compile-check OK"

# Run the LSP server from source over stdio.
lsp-run: lsp-gen
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_LSP_DIR=$(JERBOA_HOME) \
		$(SCHEME) -q --script $(LSP_ENTRY) -- --stdio

lsp-test: lsp-test-unit lsp-test-e2e

lsp-test-unit: chez lsp-gen
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_LSP_DIR=$(JERBOA_HOME) \
		$(SCHEME) -q --script $(LSP_UNIT_TEST)

# e2e launches lsp/main.ss from source; JERBOA_LSP_DIR must be the dir holding
# lsp/ (the repo root) or (lsp ...) imports double-nest.
lsp-test-e2e: chez lsp-gen
	@echo "=== LSP e2e test (source) ==="
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_LSP_DIR=$(JERBOA_HOME) SCHEME=$(SCHEME) \
		python3 $(LSP_E2E_TEST)

# Native self-contained binary for the host (./jlsp).
jlsp: chez build lsp-gen
	@echo "=== Building native ./jlsp (host) ==="
	@JERBOA_HOME=$(JERBOA_HOME) JERBOA_LSP_DIR=$(JERBOA_HOME) \
		$(SCHEME) -q --libdirs $(JERBOA_HOME) --script lsp/build-binary.ss

jlsp-install: jlsp
	@mkdir -p $(HOME)/.local/bin
	cp jlsp $(LSP_BIN)
	@echo "Installed ./jlsp -> $(LSP_BIN)"

jlsp-linux-amd64: chez build lsp-gen
	@command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { echo "ERROR: x86_64-linux-musl-gcc not on PATH" >&2; exit 1; }
	@test -d "$(JERBOA_HOME)/.chez-cross-ta6le" || { echo "ERROR: .chez-cross-ta6le not found (make chez-cross CHEZ_TARGET_MACHINE=ta6le CROSS_CC=x86_64-linux-musl-gcc)" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6le/s/xpatch" || { echo "ERROR: ta6le xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jlsp-linux-amd64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) CROSS_CC=x86_64-linux-musl-gcc \
		$(SCHEME) -q --libdirs '$(LSP_XC_LIBDIRS)' --script lsp/build-jlsp-cross.ss

jlsp-freebsd-amd64: chez build lsp-gen
	@test -x "$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" || { echo "ERROR: support/cross-cc-freebsd-amd64 not found/executable" >&2; exit 1; }
	@test -d "$(JERBOA_HOME)/.chez-cross-ta6fb" || { echo "ERROR: .chez-cross-ta6fb not found (make chez-cross CHEZ_TARGET_MACHINE=ta6fb CROSS_CC=...)" >&2; exit 1; }
	@test -f "$(CHEZ_BUILD_DIR)/xc-ta6fb/s/xpatch" || { echo "ERROR: ta6fb xpatch not found" >&2; exit 1; }
	@echo "=== Cross-building jlsp-freebsd-amd64 ==="
	@JERBOA_HOME=$(JERBOA_HOME) CROSS_CC="$(JERBOA_HOME)/support/cross-cc-freebsd-amd64" \
		$(SCHEME) -q --libdirs '$(LSP_XC_LIBDIRS)' --script lsp/build-jlsp-freebsd-cross.ss

# Host + the two cross targets.
jlsp-portable: jlsp jlsp-linux-amd64 jlsp-freebsd-amd64

test: test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-regex-all test-contract test-ergo test-limits-primitives test-typed-parser test-typed-checker test-pure-audit test-nrepl-auth test-jpkg

# jpkg package manager: run every tests/test-jpkg*.ss
.PHONY: test-jpkg
test-jpkg:
	@for t in tests/test-jpkg*.ss; do \
		[ -f "$$t" ] || continue; \
		echo "== $$t"; \
		$(SCHEME) --libdirs $(LIBDIRS) --script "$$t" || exit 1; \
	done

test-nrepl-auth:
	@if [ -f tests/test-nrepl-auth.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-nrepl-auth.ss; \
	fi

typecheck:
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typecheck.ss $(TYPED_SOURCES)

typed-rust:
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typed-rust.ss $(TYPED_RUST_DIR) $(TYPED_RUST_SOURCES)

typed-llvmir:
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typed-llvmir.ss $(TYPED_LLVMIR_DIR) $(TYPED_LLVMIR_SOURCES)

# Assemble and verify every generated .ll, then make sure it survives -O2.
# Failures print the offending .ll path so the IR can be inspected directly.
typed-llvmir-check: typed-llvmir
	@if [ -z "$(LLVM_BIN)" ]; then \
	    echo "typed-llvmir-check: LLVM tools not found (need llvm-as/opt on PATH or in a Homebrew llvm keg)" >&2; \
	    exit 1; \
	fi
	@for f in $(TYPED_LLVMIR_DIR)/*.ll; do \
	    "$(LLVM_BIN)/llvm-as" "$$f" -o "$${f%.ll}.bc" \
	        || { echo "typed-llvmir-check: llvm-as failed on $$f" >&2; exit 1; }; \
	    "$(LLVM_BIN)/opt" -passes=verify "$${f%.ll}.bc" -o /dev/null \
	        || { echo "typed-llvmir-check: verifier failed on $$f" >&2; exit 1; }; \
	    "$(LLVM_BIN)/opt" -O2 "$${f%.ll}.bc" -o "$${f%.ll}.opt.bc" \
	        || { echo "typed-llvmir-check: opt -O2 failed on $$f" >&2; exit 1; }; \
	    echo "typed-llvmir-check: OK $$f"; \
	done

# Build the smoke fixture's optimized bitcode into a real executable with
# llc + the system C compiler, run it, and require the expected exit status.
typed-llvmir-smoke: typed-llvmir-check
	@base="$(TYPED_LLVMIR_DIR)/$(TYPED_LLVMIR_SMOKE_MODULE)"; \
	"$(LLVM_BIN)/llc" -filetype=obj "$$base.opt.bc" -o "$$base.o" \
	    || { echo "typed-llvmir-smoke: llc failed on $$base.opt.bc" >&2; exit 1; }; \
	cc "$$base.o" -o "$$base" \
	    || { echo "typed-llvmir-smoke: link failed on $$base.o" >&2; exit 1; }; \
	if "$$base"; then status=0; else status=$$?; fi; \
	if [ "$$status" -eq "$(TYPED_LLVMIR_SMOKE_EXPECT)" ]; then \
	    echo "typed-llvmir-smoke: PASS — $$base exited $$status"; \
	else \
	    echo "typed-llvmir-smoke: FAIL — $$base exited $$status, expected $(TYPED_LLVMIR_SMOKE_EXPECT)" >&2; \
	    exit 1; \
	fi

# Behavior parity against the reference Rust backend: lower the same typed
# source through both backends, run both executables, and require identical
# observed results (process exit status), not identical generated source.
# The Rust side builds without cargo: rustc compiles the generated crate's
# lib.rs as an rlib plus a tiny harness main that calls the same entry.
typed-llvmir-parity: typed-llvmir-smoke
	@command -v rustc >/dev/null 2>&1 \
	    || { echo "typed-llvmir-parity: rustc not found (needed for the Rust reference build)" >&2; exit 1; }
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typed-rust.ss \
	    $(TYPED_LLVMIR_PARITY_DIR)/rust $(TYPED_LLVMIR_PARITY_SOURCE)
	@rustc --edition 2021 --crate-type rlib \
	    $(TYPED_LLVMIR_PARITY_DIR)/rust/src/lib.rs \
	    --crate-name jt_parity -o $(TYPED_LLVMIR_PARITY_DIR)/libjt_parity.rlib \
	    || { echo "typed-llvmir-parity: rustc rlib build failed" >&2; exit 1; }
	@printf 'fn main() {\n    std::process::exit(jt_parity::$(TYPED_LLVMIR_PARITY_FN)() as i32);\n}\n' \
	    > $(TYPED_LLVMIR_PARITY_DIR)/main.rs
	@rustc --edition 2021 $(TYPED_LLVMIR_PARITY_DIR)/main.rs \
	    --extern jt_parity=$(TYPED_LLVMIR_PARITY_DIR)/libjt_parity.rlib \
	    -o $(TYPED_LLVMIR_PARITY_DIR)/parity_rust \
	    || { echo "typed-llvmir-parity: rustc harness build failed" >&2; exit 1; }
	@if "$(TYPED_LLVMIR_PARITY_DIR)/parity_rust"; then rust_status=0; else rust_status=$$?; fi; \
	if "$(TYPED_LLVMIR_DIR)/$(TYPED_LLVMIR_SMOKE_MODULE)"; then llvm_status=0; else llvm_status=$$?; fi; \
	echo "typed-llvmir-parity: rust=$$rust_status llvm=$$llvm_status expected=$(TYPED_LLVMIR_SMOKE_EXPECT)"; \
	if [ "$$rust_status" -eq "$$llvm_status" ] && [ "$$rust_status" -eq "$(TYPED_LLVMIR_SMOKE_EXPECT)" ]; then \
	    echo "typed-llvmir-parity: PASS — backends agree"; \
	else \
	    echo "typed-llvmir-parity: FAIL — backend results differ" >&2; \
	    exit 1; \
	fi

typed-wrappers:
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typed-wrappers.ss $(TYPED_WRAPPER_DIR) $(TYPED_RUST_SOURCES)

typed-build: typed-rust typed-wrappers
	@cargo build --manifest-path $(TYPED_RUST_DIR)/Cargo.toml

# Project-attach: emit library-form wrappers (one .sls per typed module,
# importable as `(library-name ...)`) plus the Rust crate. Used by sibling
# projects (e.g. jerboa-code) to compile typed modules into their own libdir
# and link the resulting archive into their own binary.
#
# Required:
#   TYPED_PROJECT_SOURCES     — typed .ss source files
#   TYPED_PROJECT_WRAPPER_DIR — libdir root where .sls wrappers are written
#                               (e.g. .../src for (jcode typed pricing) →
#                               .../src/jcode/typed/pricing.sls)
#   TYPED_PROJECT_RUST_DIR    — Rust crate output directory
typed-project-build:
	@test -n "$(TYPED_PROJECT_SOURCES)" || { echo "ERROR: set TYPED_PROJECT_SOURCES" >&2; exit 1; }
	@test -n "$(TYPED_PROJECT_WRAPPER_DIR)" || { echo "ERROR: set TYPED_PROJECT_WRAPPER_DIR" >&2; exit 1; }
	@test -n "$(TYPED_PROJECT_RUST_DIR)" || { echo "ERROR: set TYPED_PROJECT_RUST_DIR" >&2; exit 1; }
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typed-rust.ss \
	  $(TYPED_PROJECT_RUST_DIR) $(TYPED_PROJECT_SOURCES)
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/typed-wrappers.ss \
	  --library $(TYPED_PROJECT_WRAPPER_DIR) $(TYPED_PROJECT_SOURCES)
	@cargo build --release --manifest-path $(TYPED_PROJECT_RUST_DIR)/Cargo.toml

typed-wrapper-smoke:
	@$(MAKE) --no-print-directory typed-build TYPED_RUST_SOURCES=tests/fixtures/typed/rust-basic.ss TYPED_WRAPPER_DIR=build/typed/jerboa-smoke
	@lib="$$(find $(TYPED_RUST_DIR)/target/debug -maxdepth 1 \( -name 'libjerboa_typed_generated.dylib' -o -name 'libjerboa_typed_generated.so' \) -print | head -n 1)"; \
	  test -n "$$lib" || { echo "ERROR: generated typed Rust dynamic library not found" >&2; exit 1; }; \
	  JERBOA_TYPED_RUST_LIB="$$lib" \
	  TYPED_WRAPPER_FILE="build/typed/jerboa-smoke/sample_typed_rust_basic.ss" \
	  $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-wrapper-e2e.ss

typed-split-tree-smoke:
	@$(MAKE) --no-print-directory typed-build TYPED_RUST_SOURCES=tests/fixtures/typed/valid-split-tree.ss TYPED_WRAPPER_DIR=build/typed/split-tree-smoke
	@lib="$$(find $(TYPED_RUST_DIR)/target/debug -maxdepth 1 \( -name 'libjerboa_typed_generated.dylib' -o -name 'libjerboa_typed_generated.so' \) -print | head -n 1)"; \
	  test -n "$$lib" || { echo "ERROR: generated typed Rust dynamic library not found" >&2; exit 1; }; \
	  JERBOA_TYPED_RUST_LIB="$$lib" \
	  TYPED_WRAPPER_FILE="build/typed/split-tree-smoke/sample_typed_split_tree.ss" \
	  $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-split-tree-e2e.ss && \
	  JERBOA_TYPED_RUST_LIB="$$lib" \
	  $(SCHEME) --libdirs $(LIBDIRS) --script support/typed-run.ss \
	    build/typed/split-tree-smoke/sample_typed_split_tree.ss \
	    tests/test-typed-split-tree-caller.ss

typed-test: test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-llvmir test-typed-wrappers test-typed-fuzz typecheck

# Build a jerboa-bin that bakes in a Typed Jerboa Rust .a archive. The
# generated wrapper resolves its `jt_*` symbols via dlsym(RTLD_DEFAULT) — no
# JERBOA_TYPED_RUST_LIB env var needed at runtime.
#
# Override:
#   TYPED_RUST_SOURCES   — typed .ss source files (default: rust-basic fixture)
#   TYPED_WRAPPER_DIR    — where the generated `.ss` wrapper is written
#   BINARY_OUTPUT        — path of the resulting jerboa-bin binary
binary-typed: chez build
	@$(MAKE) --no-print-directory typed-build \
	  TYPED_RUST_SOURCES="$(TYPED_RUST_SOURCES)" \
	  TYPED_WRAPPER_DIR="$(TYPED_WRAPPER_DIR)"
	@archive="$(TYPED_RUST_DIR)/target/debug/libjerboa_typed_generated.a"; \
	  test -f "$$archive" || { echo "ERROR: typed Rust .a not found at $$archive" >&2; exit 1; }; \
	  JERBOA_TYPED_RUST_ARCHIVE="$$archive" \
	  SCHEME=$(SCHEME) JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) \
	  support/build-binary.sh $(BINARY_ENTRY) $(BINARY_OUTPUT)

# Smoke test: prove the static-link path resolves `jt_*` symbols without
# JERBOA_TYPED_RUST_LIB by running a tiny caller through the freshly built
# binary.
binary-typed-smoke:
	@$(MAKE) --no-print-directory binary-typed \
	  TYPED_RUST_SOURCES=tests/fixtures/typed/rust-basic.ss \
	  TYPED_WRAPPER_DIR=build/typed/jerboa-binary-smoke \
	  BINARY_OUTPUT=build/typed/jerboa-bin-typed
	@unset JERBOA_TYPED_RUST_LIB; ./build/typed/jerboa-bin-typed tests/test-typed-binary-smoke.ss

typed-clean:
	@rm -rf build/typed

pure-audit:
	@$(SCHEME) --libdirs $(LIBDIRS) --script support/pure-audit.ss $(PURE_AUDIT_ARGS)

check-docs:
	@SCHEME=$(SCHEME) tools/check-doc-examples.sh

check-docs-strict:
	@SCHEME=$(SCHEME) tools/check-doc-examples.sh --strict

test-reader:
	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-reader.ss

test-core:
	@if [ -f tests/test-core.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-core.ss; \
	fi

test-runtime:
	@if [ -f tests/test-runtime.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-runtime.ss; \
	fi

test-stdlib:
	@if [ -f tests/test-stdlib.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-stdlib.ss; \
	fi

test-ffi:
	@if [ -f tests/test-ffi.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-ffi.ss; \
	fi

test-modules:
	@if [ -f tests/test-modules.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-modules.ss; \
	fi

test-expanded:
	@if [ -f tests/test-expanded-stdlib.ss ]; then \
		$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-expanded-stdlib.ss; \
	fi

test-contract:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-contract.ss

test-ergo:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-ergo.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-jerboa-prelude-ergo.ss

test-limits-primitives:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-limits-primitives.ss

test-typed-core:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-core.ss

test-typed-parser:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-parser.ss

test-typed-checker:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-checker.ss

test-typed-rust:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-rust.ss

test-typed-llvmir:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-llvmir.ss

test-typed-wrappers:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-wrappers.ss

test-typed-fuzz:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-fuzz.ss

test-pure-audit:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-pure-audit.ss

# --- Regex / rx / peg test suite ---
test-rawstring:
	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-reader-rawstring.ss

test-regex:
	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-regex.ss

test-rx:
	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-rx.ss

test-peg:
	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-peg.ss

# Run all regex-related tests in order (rawstring must pass before regex, etc.)
test-regex-all: test-rawstring test-regex test-rx test-peg

test-wrappers:
	@echo "--- Wrapper module tests ---"
	@$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrappers.ss 2>/dev/null || echo "  yaml: SKIP (library not found)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-zlib.ss 2>/dev/null \
		|| echo "  zlib: SKIP (requires jerboa_zlib_shim.so)"
	@ln -sf $(JERBOA_EXT_DIR)/jerboa-ssl/jerboa_ssl_shim.so ./jerboa_ssl_shim.so 2>/dev/null; \
		LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-ssl.ss 2>/dev/null \
		&& $(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-request.ss 2>/dev/null; \
		rm -f ./jerboa_ssl_shim.so \
		|| echo "  ssl/request: SKIP (requires jerboa_ssl_shim.so)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" JERBOA_PCRE2_LIB="$(JERBOA_EXT_DIR)/jerboa-pcre2" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-pcre2.ss 2>/dev/null \
		|| echo "  pcre2: SKIP (requires pcre2_shim.so)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-epoll.ss 2>/dev/null \
		|| echo "  epoll: SKIP (requires jerboa_epoll_shim.so)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-inotify.ss 2>/dev/null \
		|| echo "  inotify: SKIP (requires jerboa_inotify_shim.so)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-crypto.ss 2>/dev/null \
		|| echo "  crypto: SKIP (requires jerboa_crypto_shim.so)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-sqlite.ss 2>/dev/null \
		|| echo "  sqlite: SKIP (requires jerboa_sqlite_shim.so)"
	@LD_LIBRARY_PATH="$(JERBOA_EXT_LDPATH):$$LD_LIBRARY_PATH" \
		$(SCHEME) --libdirs "$(LIBDIRS):$(JERBOA_EXT_LIBDIRS)" --script tests/test-wrapper-postgresql.ss 2>/dev/null \
		|| echo "  postgresql: SKIP (requires jerboa_pg_shim.so)"

test-features: test-phase2 test-phase3

test-phase2:
	@echo "--- Phase 2 feature tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-foreign.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-channel2.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-task.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-typed-advanced.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-cache.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-effect.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-async.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-iouring.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-stm.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-ffi-bind.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-match2.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-staging.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-cluster.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-devex.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-capability.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-seq.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-table.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-concur.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-build.ss

test-phase3:
	@echo "--- Phase 3 feature tests ---"
	@echo "-- Phase 3a: Observability --"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-log.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-metrics.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-span.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-health.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-circuit.ss
	@echo "-- Phase 3b: Advanced Networking --"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-websocket.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-http2.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-dns.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-rate.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-router.ss
	@echo "-- Phase 3c: Build & Package Tooling --"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-pkg.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-lock.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-hot.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-embed.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-cross.ss
	@echo "-- Phase 3d: Language Extensions --"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-query.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-schema.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-pipeline.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-rewrite.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-lint.ss
	@echo "-- Phase 3e: WASM Target --"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-wasm-format.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-wasm-codegen.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-wasm-runtime.ss

test-phase4a:
	@echo "--- Phase 4a: Core Runtime tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-effect-deep.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-engine-pool.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-transducer.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-type-env.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-type-infer.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-error-advice.ss

test-phase4b:
	@echo "--- Phase 4b: Type System and Safety tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-hkt.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-monad.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-refine.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-solver.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-row2.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-effects-new.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-taint.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-sandbox.ss

test-phase4c:
	@echo "--- Phase 4c: Systems and Performance tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-arena.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-binary.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-mmap-btree.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-mmap.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-parallel.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-atomics.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-runtime-gc.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-multishot.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-deadlock.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-concur-util.ss

test-phase4d:
	@echo "--- Phase 4d: Developer Experience tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-timetravel.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-flamegraph.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-proptest.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-staging2.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-match-syntax.ss

test-phase4e:
	@echo "--- Phase 4e: Data and Distribution tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-dataframe.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-stream-window.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-distributed.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-wasi.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-checkpoint.ss

test-phase4f:
	@echo "--- Phase 4f: Toolchain and Interop tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-lsp.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-python.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-build-watch.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-cross-compile.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-reproducible.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-musl.ss

test-phase5: test-phase5a test-phase5b test-phase5c test-phase5d test-phase5e

test-phase5a:
	@echo "--- Phase 5a: Compiler as Library ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-cp0-passes.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-compiler-partial-eval.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-regex-compile.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-delimited.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-pgo.ss

test-phase5b:
	@echo "--- Phase 5b: Persistence and Distribution ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-persist-closure.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-persist-image.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-continuation-marks.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-coroutine.ss

test-phase5c:
	@echo "--- Phase 5c: Inspector and Debugging ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-inspector.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-closure-inspect.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-record-inspect.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-disassemble.ss

test-phase5d:
	@echo "--- Phase 5d: Advanced Effects and Concurrency ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-effect-fusion.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-stm-nested.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-async-await.ss

test-phase5e:
	@echo "--- Phase 5e: Systems and Zero-Cost ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-benchmark.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-json-schema.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-query-compile.ss

test-phase6:
	@echo "--- Phase 6: Making Real Programs Easier to Build ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase6.ss

test-phase7:
	@echo "--- Phase 7: Gerbil Porting Features ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --program tests/test-phase7.ss

test-phase8:
	@echo "--- Phase 8: Deep Gerbil Compatibility ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --program tests/test-phase8.ss

test-repl:
	@echo "--- REPL tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-repl-enhanced.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-repl-server.ss

test-functional:
	@echo "--- Functional Tests (real I/O, fork, Landlock, signals) ---"
	@gcc -shared -fPIC -O2 -o support/libjerboa-landlock.so support/landlock-shim.c 2>/dev/null || true
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-functional.ss

test-security:
	@echo "--- Security tests ---"
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-security-profile.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-crypto-random.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-crypto-compare.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-crypto-digest.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-crypto-native.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-security-capability.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-restrict-hardened.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-process-exec.ss
	@JERBOA_DB_HOST=evil.com JERBOA_DB_PORT=5433 JERBOA_SECRET=leaked $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-config-env.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-audit.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-sanitize.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase3-security.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase3-remaining.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase4-safety.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase5-os.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-phase6-supply.ss
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-security2-parsers.ss

test-security-profile:
	@$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-security-profile.ss

security: security-production

security-production: security-profile test-security-profile

security-profile:
	@test -f .jerboa/security.json

# Rust native library
RUST_NATIVE_DIR = jerboa-native-rs
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
  NATIVE_LIB_EXT = dylib
  NATIVE_LD_VAR = DYLD_LIBRARY_PATH
else
  NATIVE_LIB_EXT = so
  NATIVE_LD_VAR = LD_LIBRARY_PATH
endif
RUST_NATIVE_LIB = $(RUST_NATIVE_DIR)/target/release/libjerboa_native.$(NATIVE_LIB_EXT)

$(RUST_NATIVE_LIB): $(RUST_NATIVE_DIR)/src/*.rs $(RUST_NATIVE_DIR)/Cargo.toml
	cd $(RUST_NATIVE_DIR) && cargo build --release --features full

native: $(RUST_NATIVE_LIB)
	cp $(RUST_NATIVE_LIB) lib/
ifeq ($(UNAME_S),Darwin)
	@# Re-sign ad-hoc so dyld accepts the freshly-copied dylib.
	@# Without this, Cargo's linker-signed signature can be left in an
	@# inconsistent state (Invalid Page on dlopen → SIGKILL).
	codesign --remove-signature lib/libjerboa_native.$(NATIVE_LIB_EXT) 2>/dev/null || true
	codesign --sign - lib/libjerboa_native.$(NATIVE_LIB_EXT)
endif

clean-native:
	cd $(RUST_NATIVE_DIR) && cargo clean
	rm -f lib/libjerboa_native.$(NATIVE_LIB_EXT)

test-native: native
	@echo "--- Rust native library tests (weeks 1-4) ---"
	@$(NATIVE_LD_VAR)=lib $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-native-rust.ss
	@echo "--- Rust native library tests (weeks 5-6) ---"
	@$(NATIVE_LD_VAR)=lib $(SCHEME) --libdirs $(LIBDIRS) --script tests/test-native-rust-week5-6.ss

audit-native:
	cd $(RUST_NATIVE_DIR) && cargo audit

test-gaps:
	$(SCHEME) --libdirs $(LIBDIRS) --script tests/test-gaps.ss

test-all: test test-features test-wrappers test-security test-native test-gaps

## ========== Fuzzing ==========

FUZZ_DIR = tests/fuzz/harness
FUZZ_ITERATIONS ?= 10000

# Run all fuzz harnesses (default iterations)
fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-all.ss

# Quick smoke test for CI (~30s)
fuzz-smoke:
	FUZZ_ITERATIONS=500 $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-all.ss

# Long-running deep fuzz (nightly/dedicated)
fuzz-deep:
	FUZZ_ITERATIONS=1000000 FUZZ_MAX_SIZE=65536 $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-all.ss

# Individual fuzz targets
fuzz-reader-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-reader.ss

fuzz-json-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-json.ss

fuzz-http2-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-http2.ss

fuzz-websocket-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-websocket.ss

fuzz-dns-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-dns.ss

fuzz-pregexp-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-pregexp.ss

fuzz-csv-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-csv.ss

fuzz-base64-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-base64.ss

fuzz-hex-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-hex.ss

fuzz-uri-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-uri.ss

fuzz-format-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-format.ss

fuzz-router-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-router.ss

fuzz-sandbox-fuzz:
	FUZZ_ITERATIONS=$(FUZZ_ITERATIONS) $(SCHEME) --libdirs $(LIBDIRS) --script $(FUZZ_DIR)/fuzz-sandbox.ss

clean:
	find lib -name "*.so" -delete 2>/dev/null || true
	find lib -name "*.wpo" -delete 2>/dev/null || true

# ── Docker base image (jerboa21/jerboa) ──────────────────────────────────────
# Base image for building static musl binaries of Jerboa projects.
# Includes: stock Chez, musl Chez, jerboa lib, musl-gcc, build deps.
DOCKER_IMAGE = jerboa21/jerboa

docker: docker-build

docker-build:
	@echo "=== Building $(DOCKER_IMAGE) base image ==="
	docker build --platform linux/amd64 -t $(DOCKER_IMAGE) .
	@echo ""
	@docker images $(DOCKER_IMAGE) --format "Image: {{.Repository}}:{{.Tag}}  Size: {{.Size}}"

docker-push: docker-build
	@echo "=== Pushing $(DOCKER_IMAGE) to Docker Hub ==="
	docker push $(DOCKER_IMAGE)
	@echo "Pushed $(DOCKER_IMAGE)"
