Stop mutating source libs for portable builds

ober

0934f445c64e7b2d8d5076181e9486493f3009df

diff --git a/.gitignore b/.gitignore
index a1465f3..1d98ad6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 *.tarm64osx
 /jcode
 .jerbuild-hashes
+.build/
 jcode.json
 
 # Vendored dependencies cloned at build time by `make vendor-deps`
diff --git a/Makefile b/Makefile
index d65bba7..b863c38 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,7 @@ JERBOA_HOME ?= $(HOME)/mine/jerboa
 SCHEME     ?= $(JERBOA_HOME)/.chez/bin/scheme
 LIBDIRS    = --libdirs $(JERBOA_HOME)/lib:./lib:vendor/chez-sqlite/src:vendor/jerboa-websearch/src
 JERBUILD   = $(SCHEME) --libdirs $(JERBOA_HOME)/lib --script $(JERBOA_HOME)/jerbuild.ss
+FREEBSD_AMD64_CC ?= $(JERBOA_HOME)/support/cross-cc-freebsd-amd64
 
 # Fail early with a clear pointer if the jerboa-local Chez build is missing.
 ifeq ($(wildcard $(SCHEME)),)
@@ -323,14 +324,13 @@ test-linux: test-linux-amd64
 #
 # 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/)
+#   - FreeBSD sysroot at $(JERBOA_HOME)/.freebsd-sysroot/amd64
+#   - $(FREEBSD_AMD64_CC)
 #   - 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; \
+	@command -v $(firstword $(FREEBSD_AMD64_CC)) >/dev/null 2>&1 || { \
+	  echo "ERROR: $(FREEBSD_AMD64_CC) not found or not executable" >&2; \
 	  echo "  See top of Makefile freebsd-amd64 target for setup notes." >&2; \
 	  exit 1; }
 	@test -d $(JERBOA_HOME)/.chez-cross-ta6fb || { \
@@ -340,7 +340,7 @@ freebsd-amd64: gen
 	@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
+	JERBOA_HOME=$(JERBOA_HOME) CROSS_CC="$(FREEBSD_AMD64_CC)" $(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script build-jcode-freebsd-cross.ss
 
 # Friendly aliases.
 jcode-freebsd-amd64: freebsd-amd64
diff --git a/build-jcode-cross.ss b/build-jcode-cross.ss
index 0f19c7e..79c1f4a 100644
--- a/build-jcode-cross.ss
+++ b/build-jcode-cross.ss
@@ -195,65 +195,46 @@
 
 (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")))
+;; ── Step 0.5: isolate cross-compiled objects ───────────────────────────────
+;; Never patch source libraries in-place.  Chez xpatch can compile target
+;; objects while `library-directories` maps source directories to a separate
+;; writable object directory.
+
+(define obj-dir (format ".build/~a-obj" output))
+(define entry-so  (format "~a/program.so" obj-dir))
+(define entry-wpo (format "~a/program.wpo" obj-dir))
+
+(define (prepare-obj-dir!)
+  (system (format "rm -rf '~a'" obj-dir))
+  (system (format "mkdir -p '~a'" obj-dir)))
+
+(define (cleanup!)
+  (system (format "rm -rf '~a'" obj-dir)))
+
+(prepare-obj-dir!)
 
 ;; ── Stage 1: load xpatch (target=ta6le emit mode) ──────────────────────────
 (define orig-libdirs (library-directories))
 (printf "==> [1/6] loading xpatch (compiler -> ~a emit mode)~n" chez-machine)
 (load xpatch)
-(library-directories orig-libdirs)
+(library-directories
+  (map (lambda (pair)
+         (cons (if (pair? pair) (car pair) pair) obj-dir))
+       orig-libdirs))
 
 (compile-imported-libraries #t)
 (generate-wpo-files #t)
 
 ;; ── Stage 2: compile-program main-binary.ss ────────────────────────────────
-(printf "==> [2/6] compile-program ~a~n" entry-script)
-(guard (e [#t (restore-patched-files!) (raise e)])
-  (compile-program entry-script))
+(printf "==> [2/6] compile-program ~a -> ~a~n" entry-script entry-so)
+(guard (e [#t (cleanup!) (raise e)])
+  (compile-program entry-script entry-so))
 
 ;; ── Stage 3: compile-whole-program → wpo .so ───────────────────────────────
 (define wpo-output (string-append output ".wp.so"))
-(printf "==> [3/6] compile-whole-program main-binary.wpo -> ~a~n" wpo-output)
-(guard (e [#t (restore-patched-files!) (raise e)])
-  (compile-whole-program "main-binary.wpo" wpo-output #t))
+(printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output)
+(guard (e [#t (cleanup!) (raise e)])
+  (compile-whole-program entry-wpo wpo-output #t))
 
 ;; ── Stage 4: embed boot files + program as C arrays ────────────────────────
 (define (embed-as-c-array in-path var-name out-path)
@@ -545,17 +526,11 @@
 (printf "    ~a~n" link-cmd)
 (let ([rc (system link-cmd)])
   (unless (zero? rc)
-    (restore-patched-files!)
+    (cleanup!)
     (error 'build-jcode-cross "cross-link failed" rc)))
 
-;; ── Cleanup: restore patched files ──────────────────────────────────────────
-(restore-patched-files!)
-
-;; Clean up intermediate compile-program outputs so the next dev cycle on
-;; host scheme doesn't pick up cross-flavored .so files.
-(for-each (lambda (f)
-            (when (file-exists? f) (delete-file f)))
-  '("main-binary.so" "main-binary.wpo"))
+;; ── Cleanup: remove isolated cross objects ─────────────────────────────────
+(cleanup!)
 
 (printf "~n=== Build complete: ~a ===~n" output)
 (system (format "ls -lh ~a" output))
diff --git a/build-jcode-freebsd-cross.ss b/build-jcode-freebsd-cross.ss
index 3ad384a..0653bd0 100644
--- a/build-jcode-freebsd-cross.ss
+++ b/build-jcode-freebsd-cross.ss
@@ -8,7 +8,7 @@
 ;;; 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
+;;;   - $JERBOA_HOME/support/cross-cc-freebsd-amd64 — macOS clang+lld wrapper
 ;;;
 ;;; Produces: jcode-freebsd-amd64  (dynamic FreeBSD x86_64 ELF; depends on
 ;;; libc.so.7, libm.so, libthr.so, libutil.so on the target host).
@@ -26,7 +26,8 @@
 
 (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 cross-cc     (or (getenv "CROSS_CC")
+                         (format "~a/support/cross-cc-freebsd-amd64" jerboa-home)))
 
 (define output       "jcode-freebsd-amd64")
 (define entry-script "main-binary.ss")
@@ -144,10 +145,9 @@
                              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 ")]
+         [cc-env (format
+                  "CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER='~a' CC_x86_64_unknown_freebsd='~a' AR_x86_64_unknown_freebsd=/opt/homebrew/opt/llvm/bin/llvm-ar "
+                  cross-cc cross-cc)]
          [via-rustup
           (and rustup-bin-dir
                (format "cd '~a' && env ~aPATH='~a':$PATH RUSTC='~a/rustc' '~a/cargo' ~a"
@@ -159,8 +159,8 @@
       (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 "
+         "Required: `rustup target add x86_64-unknown-freebsd`, plus "
+         cross-cc " and "
          "/opt/homebrew/opt/llvm/bin/llvm-ar from the LLVM Homebrew formula.")))
     (unless (file-exists? jerboa-native-a)
       (error 'build-jcode-freebsd-cross "cargo succeeded but .a missing" jerboa-native-a))
@@ -183,65 +183,46 @@
 
 (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")))
+;; ── Step 0.5: isolate cross-compiled objects ───────────────────────────────
+;; Never patch source libraries in-place.  Chez xpatch can compile target
+;; objects while `library-directories` maps source directories to a separate
+;; writable object directory.
+
+(define obj-dir (format ".build/~a-obj" output))
+(define entry-so  (format "~a/program.so" obj-dir))
+(define entry-wpo (format "~a/program.wpo" obj-dir))
+
+(define (prepare-obj-dir!)
+  (system (format "rm -rf '~a'" obj-dir))
+  (system (format "mkdir -p '~a'" obj-dir)))
+
+(define (cleanup!)
+  (system (format "rm -rf '~a'" obj-dir)))
+
+(prepare-obj-dir!)
 
 ;; ── Stage 1: load xpatch (target=ta6le emit mode) ──────────────────────────
 (define orig-libdirs (library-directories))
-(printf "==> [1/6] loading xpatch (compiler -> ta6le emit mode)~n")
+(printf "==> [1/6] loading xpatch (compiler -> ta6fb emit mode)~n")
 (load xpatch)
-(library-directories orig-libdirs)
+(library-directories
+  (map (lambda (pair)
+         (cons (if (pair? pair) (car pair) pair) obj-dir))
+       orig-libdirs))
 
 (compile-imported-libraries #t)
 (generate-wpo-files #t)
 
 ;; ── Stage 2: compile-program main-binary.ss ────────────────────────────────
-(printf "==> [2/6] compile-program ~a~n" entry-script)
-(guard (e [#t (restore-patched-files!) (raise e)])
-  (compile-program entry-script))
+(printf "==> [2/6] compile-program ~a -> ~a~n" entry-script entry-so)
+(guard (e [#t (cleanup!) (raise e)])
+  (compile-program entry-script entry-so))
 
 ;; ── Stage 3: compile-whole-program → wpo .so ───────────────────────────────
 (define wpo-output (string-append output ".wp.so"))
-(printf "==> [3/6] compile-whole-program main-binary.wpo -> ~a~n" wpo-output)
-(guard (e [#t (restore-patched-files!) (raise e)])
-  (compile-whole-program "main-binary.wpo" wpo-output #t))
+(printf "==> [3/6] compile-whole-program ~a -> ~a~n" entry-wpo wpo-output)
+(guard (e [#t (cleanup!) (raise e)])
+  (compile-whole-program entry-wpo wpo-output #t))
 
 ;; ── Stage 4: embed boot files + program as C arrays ────────────────────────
 (define (embed-as-c-array in-path var-name out-path)
@@ -546,17 +527,11 @@
 (printf "    ~a~n" link-cmd)
 (let ([rc (system link-cmd)])
   (unless (zero? rc)
-    (restore-patched-files!)
+    (cleanup!)
     (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"))
+;; ── Cleanup: remove isolated cross objects ─────────────────────────────────
+(cleanup!)
 
 (printf "~n=== Build complete: ~a ===~n" output)
 (system (format "ls -lh ~a" output))
diff --git a/build-jcode-musl.ss b/build-jcode-musl.ss
index 3936450..06c2f31 100644
--- a/build-jcode-musl.ss
+++ b/build-jcode-musl.ss
@@ -112,6 +112,17 @@
 
 (printf "Jerboa lib: ~a~n" jerboa-dir)
 
+(define build-dir "/tmp/jerboa-musl-jcode-build")
+(define obj-dir (format "~a/obj" build-dir))
+(define program-so (format "~a/program.so" obj-dir))
+
+(define (prepare-build-dir!)
+  (system (format "rm -rf '~a'" build-dir))
+  (system (format "mkdir -p '~a'" obj-dir)))
+
+(define (cleanup-build-dir!)
+  (system (format "rm -rf '~a'" build-dir)))
+
 ;; ========== Auto-discovery helpers ==========
 ;;
 ;; These helpers eliminate the hardcoded parallel lists of modules that
@@ -277,52 +288,18 @@
     ;; jerboa-native (net/io) — vectored I/O wrapper used by (std net io)
     "jerboa_writev2"))
 
-;; ========== Step 0: Clean stale .so/.wpo files ==========
-
-(printf "[0/7] Cleaning stale .so/.wpo files...~n")
-(for-each (lambda (m)
-            (let ((so (format "~a.so" m))
-                  (wpo (format "~a.wpo" m)))
-              (when (file-exists? so) (delete-file so))
-              (when (file-exists? wpo) (delete-file wpo))))
-  jcode-modules)
-(system (format "find ~a -name '*.wpo' -delete 2>/dev/null" jerboa-dir))
-
-;; ========== Step 0.5: Patch libraries for static linking ==========
-
-(printf "[0.5/7] Patching libraries for static linking...~n")
-
-;; Blanket-patch ALL .sls files to replace (load-shared-object ...) with (void).
-;; In static builds, all FFI symbols are pre-registered via Sforeign_symbol —
-;; load-shared-object is both unnecessary and unsupported.
-;; This matches the approach used in jerboa-shell's musl build.
-(printf "      Blanket-patching load-shared-object in all libs...~n")
-;; Paren-aware substitution. The naive sed `[^)]*` form breaks on:
-;;   1. Nested forms:  (load-shared-object (car candidates))   <- stops at first )
-;;   2. Multi-line forms in tui-ffi.sls
-;;   3. The public helper (load-shared-object* name) in (jerboa ffi) -- this
-;;      MUST be preserved, since rewriting its body to (define (void) (void))
-;;      causes "multiple definitions for void in body" failures.
-;; The perl regex below uses a recursive named group to match balanced parens
-;; and quoted strings, and (?=\s) to require whitespace after `object` so
-;; `load-shared-object*` is left untouched.
-(define lso-patch-cmd
-  (string-append
-    "perl -i -0777 -pe 's/"
-    "\\(load-shared-object(?=\\s)"
-    "(?:[^()\"]++|\"(?:\\\\.|[^\"\\\\])*+\"|"
-    "(?<bal>\\((?:[^()\"]++|\"(?:\\\\.|[^\"\\\\])*+\"|(?&bal))*+\\))"
-    ")*+\\)/(void)/g'"))
-
-(system (format "find '~a' -name '*.sls' -exec ~a {} +" jerboa-dir lso-patch-cmd))
-(system (format "find vendor/chez-sqlite -name '*.sls' -exec ~a {} +" lso-patch-cmd))
-(system (format "find lib -name '*.sls' -exec ~a {} +" lso-patch-cmd))
-
-;; Delete ALL compiled .so in jerboa lib to force recompile with patches
-(printf "      Deleting stale .so files in all libs...~n")
-(system (format "find '~a' -name '*.so' -delete 2>/dev/null" jerboa-dir))
-(system "find vendor/chez-sqlite -name '*.so' -delete 2>/dev/null")
-(system "find lib -name '*.so' -delete 2>/dev/null")
+;; ========== Step 0: Prepare isolated object output ==========
+
+(printf "[0/7] Preparing isolated build/object directories...~n")
+(prepare-build-dir!)
+
+;; Compile imported libraries into obj-dir while loading sources from the
+;; original --libdirs. This avoids modifying $JERBOA_HOME/lib, vendor/, or
+;; generated lib/jcode files during static builds.
+(library-directories
+  (map (lambda (pair)
+         (cons (if (pair? pair) (car pair) pair) obj-dir))
+       (library-directories)))
 
 ;; ========== Step 1: Compile all modules ==========
 
@@ -338,100 +315,56 @@
                (enable-arithmetic-left-associative #t)
                (debug-level 0)
                (generate-inspector-information #f))
-  (compile-program "main-binary.ss"))
+  (compile-program "main-binary.ss" program-so))
 
-;; Fail loud if compile-program didn't actually produce main-binary.so.
+;; Fail loud if compile-program didn't actually produce the isolated program.
 ;; Historically this failure was silent — the script would continue until
-;; step 5 died with a cryptic "open-file-input-port: failed for main-binary.so".
-(unless (file-exists? "main-binary.so")
-  (printf "~n[ERROR] compile-program completed but main-binary.so was not produced.~n")
+;; step 5 died with a cryptic open-file-input-port failure.
+(unless (file-exists? program-so)
+  (printf "~n[ERROR] compile-program completed but program.so was not produced.~n")
   (printf "  cwd:   ~a~n" (current-directory))
   (printf "  entry: main-binary.ss~n")
   (printf "  This usually means an import in main-binary.ss could not be resolved~n")
-  (printf "  or a .sls file was corrupted by the blanket sed patching in step 0.5.~n")
+  (printf "  or the isolated object directory was not writable: ~a~n" obj-dir)
   (error 'build-jcode-musl
-    "main-binary.so not produced by compile-program"))
-(printf "  → main-binary.so (~a bytes)~n"
-        (file-length (open-file-input-port "main-binary.so")))
+    "program.so not produced by compile-program"))
+(printf "  → ~a (~a bytes)~n"
+        program-so
+        (file-length (open-file-input-port program-so)))
 
 ;; ========== Step 2: Skip WPO for musl builds ==========
-;; Use the direct main-binary.so from compile-program.
-(printf "[2/7] Skipping WPO (using main-binary.so directly)...~n")
-(define program-so "main-binary.so")
+;; Use the direct program.so from compile-program.
+(printf "[2/7] Skipping WPO (using isolated program.so directly)...~n")
 
-;; ========== Step 3: Auto-discover external libs ==========
-;;
-;; Historically this step held a hardcoded list of ~22 (std ...) modules to
-;; pre-compile, and step 4 held a *second* parallel list of the same modules
-;; to embed in the boot file. Both drifted every time jcode imported a new
-;; stdlib module, and the failure mode was silent (missing entries were
-;; quietly filtered out by `existing-so-files`).
+;; ========== Step 3: Auto-discover compiled libraries ==========
 ;;
-;; Instead: compile-program in step 1 already traversed main-binary.ss's full
-;; import graph with compile-imported-libraries #t, emitting a .so next to
-;; every imported .sls in $JERBOA_HOME/lib. Step 0.5 wiped that directory
-;; clean beforehand, so every .so that now exists there is one the import
-;; graph actually needs. Scan the directory to get the authoritative list.
-
-(printf "[3/7] Discovering external libs compiled under ~a...~n" jerboa-dir)
-
-;; Pick up jerboa stdlib .so files (~/mine/jerboa/lib) and any vendored
-;; library sources that were compiled as part of main-binary.ss's import
-;; graph. jerboa-websearch lives under vendor/ and is linked into jcode
-;; through (jerbsearch ...) imports; its .so files won't show up under
-;; jerboa-dir, so we scan vendor/ too. Without this, the static binary
-;; tries to load (jerbsearch query) at runtime and dies with
-;; "library (jerbsearch query) not found" because the import resolves
-;; from --libdirs at start (and a static binary has none).
-(define jerbsearch-vendor-dir "vendor/jerboa-websearch/src")
-
-(define external-lib-paths
-  (append
-    (find-files-with-suffix jerboa-dir ".so")
-    (if (file-directory? jerbsearch-vendor-dir)
-        (find-files-with-suffix jerbsearch-vendor-dir ".so")
-        '())))
-
-(printf "  Found ~a external lib .so file(s)~n" (length external-lib-paths))
-(when (null? external-lib-paths)
+;; compile-program traversed main-binary.ss's import graph and wrote every
+;; imported library object into obj-dir. Embed all of those library objects in
+;; the boot image, excluding the entry program itself.
+
+(printf "[3/7] Discovering libraries compiled under ~a...~n" obj-dir)
+
+(define compiled-lib-paths
+  (filter (lambda (p) (not (string=? p program-so)))
+          (find-files-with-suffix obj-dir ".so")))
+
+(printf "  Found ~a compiled library .so file(s)~n" (length compiled-lib-paths))
+(when (null? compiled-lib-paths)
   (error 'build-jcode-musl
-    (format "No .so files under ~a after compile-program — did step 1 fail silently?"
-            jerboa-dir)))
+    (format "No library .so files under ~a after compile-program" obj-dir)))
 
 ;; ========== Step 4: Create libs-only boot file ==========
 
 (printf "[4/7] Creating libs-only boot file...~n")
+(printf "  Boot file contents: ~a compiled library .so files~n"
+        (length compiled-lib-paths))
 
-(define chez-sqlite-so "vendor/chez-sqlite/src/chez-sqlite.so")
-(unless (file-exists? chez-sqlite-so)
-  (error 'build-jcode-musl
-    (format "chez-sqlite .so missing at ~a — was jerbuild run on vendor/?" chez-sqlite-so)))
-
-;; jcode module .so files live next to their .sls (same dir), so just swap
-;; the suffix. Strict assertion surfaces any missing entry loudly.
-(define jcode-so-files
-  (assert-so-files-exist! 'make-boot-file
-    (map (lambda (m) (format "~a.so" m)) jcode-modules)
-    "jcode module"))
-
-(printf "  Boot file contents:~n")
-(printf "    ~a external lib .so files~n" (length external-lib-paths))
-(printf "    1 chez-sqlite shim .so~n")
-(printf "    ~a jcode module .so files~n" (length jcode-so-files))
-
-(apply make-boot-file "jcode.boot" '("scheme" "petite")
-  (append
-    external-lib-paths
-    (list chez-sqlite-so)
-    jcode-so-files))
+(apply make-boot-file "jcode.boot" '("scheme" "petite") compiled-lib-paths)
 
 ;; ========== Step 5: Generate C with embedded boot files + program ==========
 
 (printf "[5/7] Generating C with embedded boot files + program...~n")
 
-(define build-dir "/tmp/jerboa-musl-jcode-build")
-(system (format "rm -rf '~a' && mkdir -p '~a'" build-dir build-dir))
-
 (define musl-lib-dir (musl-chez-lib-dir))
 (define gcc (musl-gcc-path))
 (define scheme-h-dir musl-lib-dir)
@@ -766,28 +699,13 @@
           (close-port port))
         (printf "  Wrote jcode-musl.sha256 (32 bytes)~n")))))
 
-;; ========== Restore patched files ==========
-
-(printf "~n[cleanup] Restoring patched files...~n")
-;; Restore blanket-patched jerboa lib via git checkout
-(system (format "cd '~a' && git checkout -- ." jerboa-dir-base))
-;; Restore patched local files
-(system "git checkout -- lib/jcode/")
-(system "cd vendor/chez-sqlite && git checkout -- src/chez-sqlite.sls")
-
-;; Cleanup build dir
-(system (format "rm -rf '~a'" build-dir))
-
-;; Clean up .so and .wpo from compile-imported-libraries
-(for-each (lambda (m)
-            (let ((so (format "~a.so" m))
-                  (wpo (format "~a.wpo" m)))
-              (when (file-exists? so) (delete-file so))
-              (when (file-exists? wpo) (delete-file wpo))))
-  jcode-modules)
+;; ========== Cleanup isolated build files ==========
+
+(printf "~n[cleanup] Removing isolated build directory...~n")
+(cleanup-build-dir!)
 (for-each (lambda (f)
             (when (file-exists? f) (delete-file f)))
-  '("main-binary.so" "main-binary.wpo" "jcode.boot"))
+  '("jcode.boot"))
 
 ;; Summary
 (printf "~n========================================~n")