fix: extract shared build logic to prevent cross-platform drift

ober

8d00ff9f2572df6ccad59cf3782a40a32725cc2c

diff --git a/build-common.ss b/build-common.ss
new file mode 100644
index 0000000..8356c4d
--- /dev/null
+++ b/build-common.ss
@@ -0,0 +1,168 @@
+;; build-common.ss — shared build logic for all gitsafe static binary builds.
+;;
+;; Include this file via (include "build-common.ss") AFTER defining:
+;;   jerboa-dir  — absolute path to the Jerboa home directory
+;;
+;; Provides helpers, the gitsafe module list, and the six shared build steps:
+;;   setup-library-dirs!   — Step 0: add jerboa lib to search path
+;;   do-compile!           — Step 1: compile all modules with WPO
+;;   do-wpo!               — Step 2: whole-program optimization (returns wpo-missing)
+;;   do-boot!              — Step 3: make boot file + C headers
+;;   do-cleanup!           — Step 6: remove intermediate files
+;;
+;; Steps 4 (generate C main) and 5 (compile + link) are platform-specific
+;; and live in the including script.
+
+;; --- C byte-array header generation ---
+(define (file->c-header input-path output-path array-name size-name)
+  (let* ([port (open-file-input-port input-path)]
+         [data (get-bytevector-all port)]
+         [size (bytevector-length data)])
+    (close-port port)
+    (call-with-output-file output-path
+      (lambda (out)
+        (fprintf out "/* Auto-generated — do not edit */\n")
+        (fprintf out "static const unsigned char ~a[] = {\n" array-name)
+        (let loop ([i 0])
+          (when (< i size)
+            (when (= 0 (modulo i 16)) (fprintf out "  "))
+            (fprintf out "0x~2,'0x" (bytevector-u8-ref data i))
+            (when (< (+ i 1) size) (fprintf out ","))
+            (when (= 15 (modulo i 16)) (fprintf out "\n"))
+            (loop (+ i 1))))
+        (fprintf out "\n};\n")
+        (fprintf out "static const unsigned int ~a = ~a;\n" size-name size))
+      'replace)
+    (printf "  ~a: ~a bytes\n" output-path size)))
+
+;; --- Find the csv<version>/<machine-type> dir inside a Chez lib directory ---
+(define (find-csv-dir lib-dir mt)
+  (let ([csv-dir
+          (let lp ([dirs (guard (e [#t '()]) (directory-list lib-dir))])
+            (cond
+              [(null? dirs) #f]
+              [(and (> (string-length (car dirs)) 3)
+                    (string=? "csv" (substring (car dirs) 0 3)))
+               (format "~a/~a/~a" lib-dir (car dirs) mt)]
+              [else (lp (cdr dirs))]))])
+    (and csv-dir
+         (file-exists? (format "~a/main.o" csv-dir))
+         csv-dir)))
+
+;; --- Filter a path list to files that actually exist ---
+(define (existing-so-files paths)
+  (filter file-exists? paths))
+
+;; --- Convert a library name to its .so path under jerboa-dir ---
+;; e.g. (std misc string) → <jerboa-dir>/lib/std/misc/string.so
+;; References `jerboa-dir` from the including script's scope.
+(define (lib-name->so-path lib-name)
+  (let* ([parts (map symbol->string lib-name)]
+         [rel   (let loop ([ps parts] [acc ""])
+                  (if (null? ps)
+                    acc
+                    (loop (cdr ps)
+                          (if (string=? acc "")
+                            (car ps)
+                            (string-append acc "/" (car ps))))))]
+         [so    (format "~a/lib/~a.so" jerboa-dir rel)])
+    (and (file-exists? so) so)))
+
+;; --- gitsafe application modules (same on all platforms) ---
+(define gitsafe-modules
+  '("gitsafe/entropy"
+    "gitsafe/config"
+    "gitsafe/allowlist"
+    "gitsafe/patterns"
+    "gitsafe/git"
+    "gitsafe/scanner"
+    "gitsafe/output"))
+
+;; --- Step 0: add library search paths ---
+(define (setup-library-dirs!)
+  (library-directories
+    (append
+      (list (cons (current-directory) (current-directory))
+            (cons (format "~a/lib" jerboa-dir)
+                  (format "~a/lib" jerboa-dir)))
+      (library-directories))))
+
+;; --- Step 1: compile all modules (optimize-level 3, WPO) ---
+(define (do-compile!)
+  (printf "\n[1/6] Compiling all modules (optimize-level 3, WPO)...\n")
+  (parameterize ([compile-imported-libraries         #t]
+                 [optimize-level                     3]
+                 [cp0-effort-limit                   500]
+                 [cp0-score-limit                    50]
+                 [cp0-outer-unroll-limit             1]
+                 [commonization-level                4]
+                 [enable-unsafe-application          #t]
+                 [enable-unsafe-variable-reference   #t]
+                 [enable-arithmetic-left-associative #t]
+                 [debug-level                        0]
+                 [generate-inspector-information     #f]
+                 [generate-wpo-files                 #t])
+    (compile-program "gitsafe/main-binary.ss")))
+
+;; --- Step 2: whole-program optimization ---
+;; Returns the list of libraries not incorporated (no .wpo file available).
+;; The caller passes this to do-boot! so they are bundled as .so files instead.
+(define (do-wpo!)
+  (printf "[2/6] Running whole-program optimization...\n")
+  (let ([wpo-missing (compile-whole-program "gitsafe/main-binary.wpo" "gitsafe-all.so")])
+    (unless (null? wpo-missing)
+      (printf "  WPO: ~a libraries not incorporated (missing .wpo) — will bundle .so files:\n"
+              (length wpo-missing))
+      (for-each (lambda (lib) (printf "    ~a\n" lib)) wpo-missing))
+    wpo-missing))
+
+;; --- Step 3: create boot file and C headers ---
+;; wpo-missing  — list returned by do-wpo!
+;; chez-boot-dir — directory containing petite.boot and scheme.boot
+;;                 (the platform Chez install dir, NOT necessarily the host Chez)
+(define (do-boot! wpo-missing chez-boot-dir)
+  (printf "[3/6] Creating boot file and C headers...\n")
+  ;; Find .so files for any stdlib lib that WPO couldn't inline.
+  (let ([missing-sos
+         (let loop ([libs wpo-missing] [acc '()])
+           (if (null? libs)
+             (reverse acc)
+             (let ([so (lib-name->so-path (car libs))])
+               (loop (cdr libs) (if so (cons so acc) acc)))))])
+    (when (not (null? missing-sos))
+      (printf "  Bundling ~a stdlib .so files into boot image.\n" (length missing-sos)))
+    (apply make-boot-file "gitsafe.boot" '("scheme" "petite")
+      (existing-so-files
+        (append
+          (map (lambda (m) (format "~a.so" m)) gitsafe-modules)
+          missing-sos))))
+  (file->c-header "gitsafe-all.so"
+                  "gitsafe_program.h"
+                  "gitsafe_program_data" "gitsafe_program_size")
+  (file->c-header (format "~a/petite.boot" chez-boot-dir)
+                  "gitsafe_petite_boot.h"
+                  "petite_boot_data" "petite_boot_size")
+  (file->c-header (format "~a/scheme.boot" chez-boot-dir)
+                  "gitsafe_scheme_boot.h"
+                  "scheme_boot_data" "scheme_boot_size")
+  (file->c-header "gitsafe.boot"
+                  "gitsafe_boot.h"
+                  "gitsafe_boot_data" "gitsafe_boot_size"))
+
+;; --- Step 6: remove intermediate files ---
+;; platform: "macos" or "musl" — determines the C source/object file names
+(define (do-cleanup! platform)
+  (printf "[6/6] Cleaning up intermediate files...\n")
+  (for-each (lambda (f) (when (file-exists? f) (delete-file f)))
+    (list (format "gitsafe-main-~a.c" platform)
+          (format "gitsafe-main-~a.o" platform)
+          "gitsafe_program.h" "gitsafe_petite_boot.h"
+          "gitsafe_scheme_boot.h" "gitsafe_boot.h"
+          "gitsafe-all.so" "gitsafe.boot"
+          "gitsafe/main-binary.wpo" "gitsafe/main-binary.so"))
+  (for-each (lambda (m)
+              (for-each (lambda (ext)
+                          (let ([f (format "~a~a" m ext)])
+                            (when (file-exists? f) (delete-file f))))
+                        '(".so" ".wpo")))
+            gitsafe-modules))
diff --git a/build-gitsafe-macos.sh b/build-gitsafe-macos.sh
index 0a9fe70..9b94e5f 100755
--- a/build-gitsafe-macos.sh
+++ b/build-gitsafe-macos.sh
@@ -84,6 +84,7 @@ if [ -f "gitsafe-macos" ]; then
     otool -L gitsafe-macos 2>/dev/null | tail -n +2 || true
     echo ""
     echo "Test: ./gitsafe-macos --version"
+    ./gitsafe-macos --version || { echo "ERROR: binary smoke test failed"; exit 1; }
 else
     echo "ERROR: gitsafe-macos not created"
     exit 1
diff --git a/build-gitsafe-macos.ss b/build-gitsafe-macos.ss
index aeb45b2..4f1b887 100644
--- a/build-gitsafe-macos.ss
+++ b/build-gitsafe-macos.ss
@@ -14,42 +14,12 @@
 
 (import (chezscheme))
 
-;; --- Helper: generate C byte-array from binary file ---
-(define (file->c-header input-path output-path array-name size-name)
-  (let* ([port (open-file-input-port input-path)]
-         [data (get-bytevector-all port)]
-         [size (bytevector-length data)])
-    (close-port port)
-    (call-with-output-file output-path
-      (lambda (out)
-        (fprintf out "/* Auto-generated — do not edit */\n")
-        (fprintf out "static const unsigned char ~a[] = {\n" array-name)
-        (let loop ([i 0])
-          (when (< i size)
-            (when (= 0 (modulo i 16)) (fprintf out "  "))
-            (fprintf out "0x~2,'0x" (bytevector-u8-ref data i))
-            (when (< (+ i 1) size) (fprintf out ","))
-            (when (= 15 (modulo i 16)) (fprintf out "\n"))
-            (loop (+ i 1))))
-        (fprintf out "\n};\n")
-        (fprintf out "static const unsigned int ~a = ~a;\n" size-name size))
-      'replace)
-    (printf "  ~a: ~a bytes\n" output-path size)))
-
-;; --- Locate Chez install directory ---
-(define (find-csv-dir lib-dir mt)
-  (let ([csv-dir
-          (let lp ([dirs (guard (e [#t '()]) (directory-list lib-dir))])
-            (cond
-              [(null? dirs) #f]
-              [(and (> (string-length (car dirs)) 3)
-                    (string=? "csv" (substring (car dirs) 0 3)))
-               (format "~a/~a/~a" lib-dir (car dirs) mt)]
-              [else (lp (cdr dirs))]))])
-    (and csv-dir
-         (file-exists? (format "~a/main.o" csv-dir))
-         csv-dir)))
+;; Load shared build logic (defines find-csv-dir and all step functions).
+;; jerboa-dir must be defined before any of the shared step functions are CALLED
+;; (not before this include — lambdas capture it lazily).
+(include "build-common.ss")
 
+;; --- Locate Chez install directory (macOS) ---
 (define chez-dir
   (or (getenv "CHEZ_DIR")
       (let ([mt   (symbol->string (machine-type))]
@@ -63,7 +33,6 @@
   (exit 1))
 
 ;; --- Locate Jerboa ---
-(define home (getenv "HOME"))
 (define jerboa-dir
   (or (getenv "JERBOA_HOME")
       (let ([sibling (format "~a/../jerboa" (current-directory))])
@@ -77,86 +46,13 @@
 (printf "Jerboa dir:    ~a\n" jerboa-dir)
 (printf "Machine type:  ~a\n" (machine-type))
 
-;; Add library paths
-(library-directories
-  (append
-    (list (cons (current-directory) (current-directory))
-          (cons (format "~a/lib" jerboa-dir)
-                (format "~a/lib" jerboa-dir)))
-    (library-directories)))
-
-;; --- Step 1: Compile all modules (optimize-level 3, WPO) ---
-(printf "\n[1/6] Compiling all modules (optimize-level 3, WPO)...\n")
-(parameterize ([compile-imported-libraries  #t]
-               [optimize-level              3]
-               [cp0-effort-limit            500]
-               [cp0-score-limit             50]
-               [cp0-outer-unroll-limit      1]
-               [commonization-level         4]
-               [enable-unsafe-application   #t]
-               [enable-unsafe-variable-reference #t]
-               [enable-arithmetic-left-associative #t]
-               [debug-level                 0]
-               [generate-inspector-information #f]
-               [generate-wpo-files          #t])
-  (compile-program "gitsafe/main-binary.ss"))
-
-;; --- Step 2: Whole-program optimization ---
-(printf "[2/6] Running whole-program optimization...\n")
-(let ([missing (compile-whole-program "gitsafe/main-binary.wpo" "gitsafe-all.so")])
-  (unless (null? missing)
-    (printf "  WPO: ~a libraries not incorporated (missing .wpo):\n" (length missing))
-    (for-each (lambda (lib) (printf "    ~a\n" lib)) missing)))
-
-;; --- Step 3: Create boot file + C headers ---
-(printf "[3/6] Creating boot file and C headers...\n")
-
-(define (existing-so-files paths)
-  (filter file-exists? paths))
-
-;; Standard library modules that WPO can't inline (no .wpo files).
-;; These must be included in the boot file so the binary is self-contained.
-;; Order matters: dependencies before dependents.
-(define jerboa-std-modules
-  (map (lambda (m) (format "~a/lib/~a.so" jerboa-dir m))
-    '("std/pregexp"
-      "std/os/path"
-      "std/text/json"
-      "std/misc/list"
-      "std/misc/process"
-      "std/misc/ports"
-      "std/misc/string"
-      "jerboa/runtime")))
+;; --- Steps 0–3: shared compile + WPO + boot file ---
+(setup-library-dirs!)
+(do-compile!)
+(define wpo-missing (do-wpo!))
+(do-boot! wpo-missing chez-dir)
 
-(define gitsafe-modules
-  '("gitsafe/entropy"
-    "gitsafe/config"
-    "gitsafe/allowlist"
-    "gitsafe/patterns"
-    "gitsafe/git"
-    "gitsafe/scanner"
-    "gitsafe/output"))
-
-(apply make-boot-file "gitsafe.boot" '("scheme" "petite")
-  (append
-    (existing-so-files jerboa-std-modules)
-    (existing-so-files
-      (map (lambda (m) (format "~a.so" m)) gitsafe-modules))))
-
-(file->c-header "gitsafe-all.so"
-                "gitsafe_program.h"
-                "gitsafe_program_data" "gitsafe_program_size")
-(file->c-header (format "~a/petite.boot" chez-dir)
-                "gitsafe_petite_boot.h"
-                "petite_boot_data" "petite_boot_size")
-(file->c-header (format "~a/scheme.boot" chez-dir)
-                "gitsafe_scheme_boot.h"
-                "scheme_boot_data" "scheme_boot_size")
-(file->c-header "gitsafe.boot"
-                "gitsafe_boot.h"
-                "gitsafe_boot_data" "gitsafe_boot_size")
-
-;; --- Step 4: Generate C main ---
+;; --- Step 4: Generate C main (macOS — no dlopen stubs needed) ---
 (printf "[4/6] Generating C main...\n")
 
 (call-with-output-file "gitsafe-main-macos.c"
@@ -197,15 +93,13 @@
     (fprintf out "}\n"))
   'replace)
 
-;; --- Step 5: Compile and link (maximally static) ---
+;; --- Step 5: Compile and link (maximally static, macOS) ---
 (printf "[5/6] Compiling and linking (maximally static)...\n")
 
-;; Find static .a archives bundled with Chez
 (define kernel-a (format "~a/libkernel.a" chez-dir))
 (define chez-lz4-a (format "~a/liblz4.a" chez-dir))
 (define chez-z-a (format "~a/libz.a" chez-dir))
 
-;; Verify Chez static libs exist
 (for-each
   (lambda (pair)
     (unless (file-exists? (cdr pair))
@@ -215,7 +109,6 @@
         (cons "liblz4.a" chez-lz4-a)
         (cons "libz.a" chez-z-a)))
 
-;; Find ncurses static lib (from homebrew or env)
 (define ncurses-a
   (or (let ([p (getenv "NCURSES_STATIC_PATH")])
         (and p (> (string-length p) 0) (file-exists? p) p))
@@ -230,7 +123,6 @@
 (printf "    zlib:    ~a\n" chez-z-a)
 (printf "    ncurses: ~a\n" (or ncurses-a "(dynamic fallback)"))
 
-;; Build link command: static .a files + system dynamic libs
 (define static-libs
   (string-append
     kernel-a " " chez-lz4-a " " chez-z-a
@@ -242,41 +134,21 @@
     " -liconv -lpthread -lm"))
 
 (let ([cc (or (getenv "CC") "cc")])
-  ;; Compile
   (let ([rc (system (format "~a -c -O2 -I~a -o gitsafe-main-macos.o gitsafe-main-macos.c"
                             cc chez-dir))])
     (unless (= rc 0) (printf "Error: C compilation failed\n") (exit 1)))
-  ;; Link: our main + static archives + system dylibs
-  ;; Note: Chez main.o is NOT included — it contains its own main() entry point.
-  ;; We provide our own main() that sets up embedded boot files.
   (let* ([cmd (format "~a -o gitsafe-macos gitsafe-main-macos.o ~a~a"
                       cc static-libs dynamic-libs)])
     (printf "  Link: ~a\n" cmd)
     (let ([rc (system cmd)])
       (unless (= rc 0) (printf "Error: linking failed\n") (exit 1)))))
 
-;; Strip
 (printf "  Stripping binary...\n")
 (system "strip -x gitsafe-macos")
-
-;; SHA256
 (system "shasum -a 256 gitsafe-macos > gitsafe-macos.sha256")
 
 ;; --- Step 6: Cleanup ---
-(printf "[6/6] Cleaning up intermediate files...\n")
-(for-each (lambda (f) (when (file-exists? f) (delete-file f)))
-  '("gitsafe-main-macos.c" "gitsafe-main-macos.o"
-    "gitsafe_program.h" "gitsafe_petite_boot.h"
-    "gitsafe_scheme_boot.h" "gitsafe_boot.h"
-    "gitsafe-all.so" "gitsafe.boot"
-    "gitsafe/main-binary.wpo" "gitsafe/main-binary.so"))
-
-(for-each (lambda (m)
-            (for-each (lambda (ext)
-                        (let ([f (format "~a~a" m ext)])
-                          (when (file-exists? f) (delete-file f))))
-                      '(".so" ".wpo")))
-          gitsafe-modules)
+(do-cleanup! "macos")
 
 (printf "\nDone! Binary: ./gitsafe-macos\n")
 (printf "  Size:   ")
diff --git a/build-gitsafe-musl.sh b/build-gitsafe-musl.sh
index 03ba5e9..62d6528 100755
--- a/build-gitsafe-musl.sh
+++ b/build-gitsafe-musl.sh
@@ -60,6 +60,7 @@ if [ -f "gitsafe-musl" ]; then
     ldd gitsafe-musl 2>&1 || echo "  (Fully static — no dynamic dependencies)"
     echo ""
     echo "Test: ./gitsafe-musl --version"
+    ./gitsafe-musl --version || { echo "ERROR: binary smoke test failed"; exit 1; }
 else
     echo "ERROR: gitsafe-musl not created"
     exit 1
diff --git a/build-gitsafe-musl.ss b/build-gitsafe-musl.ss
index 6070561..b90b0ad 100644
--- a/build-gitsafe-musl.ss
+++ b/build-gitsafe-musl.ss
@@ -1,40 +1,23 @@
 #!chezscheme
 ;; Build gitsafe as a fully static binary using musl libc.
 ;;
-;; Usage: make gitsafe-musl-local
+;; Usage: make linux-local
 ;;   (runs via build-gitsafe-musl.sh → this script)
 ;;
 ;; Prerequisites:
 ;;   - musl-gcc installed (apt install musl-tools)
 ;;   - Chez Scheme built with: ./configure --threads --static CC=musl-gcc
 ;;     installed to ~/chez-musl (or set JERBOA_MUSL_CHEZ_PREFIX)
-;;   - Stock scheme (glibc) for compilation steps
+;;   - Stock scheme (glibc) for the compilation steps
 ;;
 ;; Produces: ./gitsafe-musl (fully static ELF binary, zero runtime dependencies)
 
 (import (chezscheme))
 
-;; --- Helper: generate C byte-array from binary file ---
-(define (file->c-header input-path output-path array-name size-name)
-  (let* ([port (open-file-input-port input-path)]
-         [data (get-bytevector-all port)]
-         [size (bytevector-length data)])
-    (close-port port)
-    (call-with-output-file output-path
-      (lambda (out)
-        (fprintf out "/* Auto-generated — do not edit */\n")
-        (fprintf out "static const unsigned char ~a[] = {\n" array-name)
-        (let loop ([i 0])
-          (when (< i size)
-            (when (= 0 (modulo i 16)) (fprintf out "  "))
-            (fprintf out "0x~2,'0x" (bytevector-u8-ref data i))
-            (when (< (+ i 1) size) (fprintf out ","))
-            (when (= 15 (modulo i 16)) (fprintf out "\n"))
-            (loop (+ i 1))))
-        (fprintf out "\n};\n")
-        (fprintf out "static const unsigned int ~a = ~a;\n" size-name size))
-      'replace)
-    (printf "  ~a: ~a bytes\n" output-path size)))
+;; Load shared build logic (defines find-csv-dir and all step functions).
+;; jerboa-dir must be defined before any of the shared step functions are CALLED
+;; (not before this include — lambdas capture it lazily).
+(include "build-common.ss")
 
 ;; --- Locate musl-built Chez Scheme ---
 (define musl-chez-prefix
@@ -49,19 +32,6 @@
   (display "  See: https://github.com/ober/ChezScheme (build with --static CC=musl-gcc)\n")
   (exit 1))
 
-(define (find-csv-dir lib-dir mt)
-  (let ([csv-dir
-          (let lp ([dirs (guard (e [#t '()]) (directory-list lib-dir))])
-            (cond
-              [(null? dirs) #f]
-              [(and (> (string-length (car dirs)) 3)
-                    (string=? "csv" (substring (car dirs) 0 3)))
-               (format "~a/~a/~a" lib-dir (car dirs) mt)]
-              [else (lp (cdr dirs))]))])
-    (and csv-dir
-         (file-exists? (format "~a/main.o" csv-dir))
-         csv-dir)))
-
 (define musl-chez-dir
   (let ([mt (symbol->string (machine-type))])
     (or (find-csv-dir (format "~a/lib" musl-chez-prefix) mt)
@@ -73,7 +43,6 @@
           (exit 1)))))
 
 ;; --- Locate Jerboa ---
-(define home (getenv "HOME"))
 (define jerboa-dir
   (or (getenv "JERBOA_HOME")
       (let ([sibling (format "~a/../jerboa" (current-directory))])
@@ -87,71 +56,26 @@
 (printf "Jerboa dir:    ~a\n" jerboa-dir)
 (printf "Machine type:  ~a\n" (machine-type))
 
-;; Add library paths (stock Chez for compilation)
-(library-directories
-  (append
-    (list (cons (current-directory) (current-directory))
-          (cons (format "~a/lib" jerboa-dir)
-                (format "~a/lib" jerboa-dir)))
-    (library-directories)))
-
-;; --- Step 1: Compile all modules (optimize-level 3, WPO) ---
-(printf "\n[1/6] Compiling all modules (optimize-level 3, WPO)...\n")
-(parameterize ([compile-imported-libraries  #t]
-               [optimize-level              3]
-               [cp0-effort-limit            500]
-               [cp0-score-limit             50]
-               [cp0-outer-unroll-limit      1]
-               [commonization-level         4]
-               [enable-unsafe-application   #t]
-               [enable-unsafe-variable-reference #t]
-               [enable-arithmetic-left-associative #t]
-               [debug-level                 0]
-               [generate-inspector-information #f]
-               [generate-wpo-files          #t])
-  (compile-program "gitsafe/main-binary.ss"))
-
-;; --- Step 2: Whole-program optimization ---
-(printf "[2/6] Running whole-program optimization...\n")
-(let ([missing (compile-whole-program "gitsafe/main-binary.wpo" "gitsafe-all.so")])
-  (unless (null? missing)
-    (printf "  WPO: ~a libraries not incorporated (missing .wpo):\n" (length missing))
-    (for-each (lambda (lib) (printf "    ~a\n" lib)) missing)))
-
-;; --- Step 3: Create boot file + C headers ---
-(printf "[3/6] Creating boot file and C headers...\n")
-
-(define (existing-so-files paths)
-  (filter file-exists? paths))
-
-(define gitsafe-modules
-  '("gitsafe/entropy"
-    "gitsafe/config"
-    "gitsafe/allowlist"
-    "gitsafe/patterns"
-    "gitsafe/git"
-    "gitsafe/scanner"
-    "gitsafe/output"))
+;; --- Steps 0–3: shared compile + WPO + boot file ---
+;; Boot files come from the musl Chez (ABI must match the musl kernel).
+(setup-library-dirs!)
+(do-compile!)
+(define wpo-missing (do-wpo!))
+(do-boot! wpo-missing musl-chez-dir)
 
-(apply make-boot-file "gitsafe.boot" '("scheme" "petite")
-  (existing-so-files
-    (map (lambda (m) (format "~a.so" m)) gitsafe-modules)))
-
-;; Embed musl Chez boot files (must match musl kernel for ABI compatibility)
-(file->c-header "gitsafe-all.so"
-                "gitsafe_program.h"
-                "gitsafe_program_data" "gitsafe_program_size")
-(file->c-header (format "~a/petite.boot" musl-chez-dir)
-                "gitsafe_petite_boot.h"
-                "petite_boot_data" "petite_boot_size")
-(file->c-header (format "~a/scheme.boot" musl-chez-dir)
-                "gitsafe_scheme_boot.h"
-                "scheme_boot_data" "scheme_boot_size")
-(file->c-header "gitsafe.boot"
-                "gitsafe_boot.h"
-                "gitsafe_boot_data" "gitsafe_boot_size")
-
-;; --- Step 4: Generate C main with embedded program ---
+;; --- Step 4: Generate C main (musl — with dlopen stubs + Sforeign_symbol) ---
+;;
+;; dlopen(NULL, ...) returns a fake self-handle so Chez can query its own
+;; symbol table. All other dlopen calls return NULL, causing
+;; (load-shared-object "libjerboa_native.so") in (std regex) to throw an
+;; exception that the guard catches → native-available? = #f → all regex
+;; falls back to the pure-Scheme pregexp engine.
+;;
+;; Sforeign_symbol MUST be called after Sbuild_heap (the foreign entry table
+;; is not initialized until then). We register the three Rust regex symbols
+;; with a harmless C stub so (std regex)'s (foreign-procedure ...) forms
+;; succeed at WPO program init. The stub returns -1 but is never called
+;; because native-available? = #f prevents all native regex code paths.
 (printf "[4/6] Generating C main...\n")
 
 (call-with-output-file "gitsafe-main-musl.c"
@@ -168,18 +92,6 @@
     (fprintf out "#include \"gitsafe_boot.h\"\n")
     (fprintf out "#include \"gitsafe_program.h\"\n")
     (fprintf out "\n")
-    ;; dlopen/dlsym stubs for fully static musl build.
-    ;;
-    ;; dlopen(NULL, ...) returns a fake self-handle so Chez can query its own
-    ;; symbol table. All other dlopen calls return NULL, causing
-    ;; (load-shared-object "libjerboa_native.so") in (std regex) to throw an
-    ;; exception that the guard catches → native-available? = #f → all regex
-    ;; falls back to the pure-Scheme pregexp engine.
-    ;;
-    ;; dlsym returns a harmless stub for the three known regex foreign-procedure
-    ;; symbols in case Chez resolves them eagerly on this build (empirically
-    ;; it can). The stub returns -1 but is never called because native-available?
-    ;; = #f prevents any code path that would invoke c-native-compile/find/free.
     (fprintf out "/* dlopen/dlsym stubs — fully static musl binary, no dynamic libraries */\n")
     (fprintf out "static int _jerboa_native_stub(void) { return -1; }\n")
     (fprintf out "void *dlopen(const char *f, int m) { (void)m; return (!f) ? (void*)1 : NULL; }\n")
@@ -212,12 +124,6 @@
     (fprintf out "  Sregister_boot_file_bytes(\"scheme\", (void*)scheme_boot_data, scheme_boot_size);\n")
     (fprintf out "  Sregister_boot_file_bytes(\"gitsafe\", (void*)gitsafe_boot_data, gitsafe_boot_size);\n")
     (fprintf out "  Sbuild_heap(NULL, NULL);\n")
-    ;; Sforeign_symbol MUST be called after Sbuild_heap (foreign entry table is
-    ;; not initialized until then). We register the three Rust regex symbols with
-    ;; a harmless C stub so that (std regex)'s (foreign-procedure ...) definitions
-    ;; succeed when the WPO program initializes. The stub returns -1 but is never
-    ;; called: dlopen("libjerboa_native.so") returns NULL (our stub), so
-    ;; native-available? = #f, which prevents any call to c-native-compile/find/free.
     (fprintf out "  /* Register regex native symbols AFTER Sbuild_heap */\n")
     (fprintf out "  Sforeign_symbol(\"jerboa_regex_compile\", (void *)_jerboa_native_stub);\n")
     (fprintf out "  Sforeign_symbol(\"jerboa_regex_find\",    (void *)_jerboa_native_stub);\n")
@@ -229,7 +135,7 @@
     (fprintf out "}\n"))
   'replace)
 
-;; --- Step 5: Compile and link with musl-gcc ---
+;; --- Step 5: Compile and link with musl-gcc (fully static) ---
 (printf "[5/6] Compiling and linking with musl-gcc (static)...\n")
 
 (define link-libs "-lkernel -llz4 -lz -lm -ldl -lpthread")
@@ -242,26 +148,12 @@
                           musl-chez-dir link-libs))])
   (unless (= rc 0) (printf "Error: linking failed\n") (exit 1)))
 
-;; Strip and generate integrity hash
 (printf "  Stripping binary...\n")
 (system "strip --strip-all gitsafe-musl")
 (system "sha256sum gitsafe-musl > gitsafe-musl.sha256")
 
 ;; --- Step 6: Cleanup ---
-(printf "[6/6] Cleaning up intermediate files...\n")
-(for-each (lambda (f) (when (file-exists? f) (delete-file f)))
-  '("gitsafe-main-musl.c" "gitsafe-main-musl.o"
-    "gitsafe_program.h" "gitsafe_petite_boot.h"
-    "gitsafe_scheme_boot.h" "gitsafe_boot.h"
-    "gitsafe-all.so" "gitsafe.boot"
-    "gitsafe/main-binary.wpo" "gitsafe/main-binary.so"))
-
-(for-each (lambda (m)
-            (for-each (lambda (ext)
-                        (let ([f (format "~a~a" m ext)])
-                          (when (file-exists? f) (delete-file f))))
-                      '(".so" ".wpo")))
-          gitsafe-modules)
+(do-cleanup! "musl")
 
 (printf "\nDone! Binary: ./gitsafe-musl\n")
 (printf "  Size:   ")