Add macOS build target with maximally-static linking
ober
0a75441a53aee0a14903bfe1456af3b9a4a38011
--- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ gitsafe-bin gitsafe-musl gitsafe-musl.sha256 +gitsafe-macos +gitsafe-macos.sha256 *.so *.wpo --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ TEMPLATE_DIR := $(HOME)/.git-templates HOOK_DIR := $(TEMPLATE_DIR)/hooks .PHONY: run test binary install clean linux linux-local docker \ - verify-harden help install-native + verify-harden help install-native macos gitsafe-macos run: JERBOA_HOME=$(JERBOA_HOME) \ @@ -19,10 +19,15 @@ test: JERBOA_HOME=$(JERBOA_HOME) \ $(SCHEME) -q --libdirs $(CURDIR):$(JERBOA_HOME)/lib --script test/test-gitsafe.ss -install: linux +install: $(if $(filter Darwin,$(shell uname -s)),gitsafe-macos,linux) mkdir -p $(BIN_DIR) +ifeq ($(shell uname -s),Darwin) + cp gitsafe-macos $(BIN_DIR)/gitsafe + @echo "Installed gitsafe to $(BIN_DIR)/gitsafe (macOS binary)" +else cp gitsafe-musl $(BIN_DIR)/gitsafe @echo "Installed gitsafe to $(BIN_DIR)/gitsafe (static binary)" +endif mkdir -p $(HOOK_DIR) printf '#!/bin/sh\nexec gitsafe pre-commit\n' > $(HOOK_DIR)/pre-commit chmod +x $(HOOK_DIR)/pre-commit @@ -43,6 +48,17 @@ install-native: binary cp gitsafe-bin $(BIN_DIR)/gitsafe @echo "Installed gitsafe-bin to $(BIN_DIR)/gitsafe (native, requires Chez runtime)" +# ── macOS binary (maximally static) ───────────────────────────────────────── +# Statically links Chez kernel, lz4, zlib, ncurses. +# Only libSystem (always present on macOS) and libiconv are dynamic. +# Use `make macos` or `make gitsafe-macos` to build on macOS. + +macos: gitsafe-macos + +gitsafe-macos: + JERBOA_HOME=$(JERBOA_HOME) \ + ./build-gitsafe-macos.sh + # ── Static musl binary ────────────────────────────────────────────────────── # Use `make linux` to build in Docker (canonical, reproducible). # Use `make linux-local` to build directly on the host (requires @@ -85,7 +101,7 @@ verify-harden: linux clean: find . -name '*.so' -delete find . -name '*.wpo' -delete - rm -f gitsafe-bin gitsafe-musl gitsafe-musl.sha256 + rm -f gitsafe-bin gitsafe-musl gitsafe-musl.sha256 gitsafe-macos gitsafe-macos.sha256 # ── Help ───────────────────────────────────────────────────────────────────── help: @@ -95,10 +111,11 @@ help: @echo " make run ARGS='...' Run gitsafe in interpreter mode" @echo " make test Run test suite" @echo "" - @echo "Build & install (static binary via Docker, zero runtime deps):" - @echo " make linux Docker build (canonical, reproducible)" - @echo " make linux-local Local build (requires musl-gcc + musl Chez)" - @echo " make install Docker build + install to ~/.local/bin" + @echo "Build & install:" + @echo " make macos macOS build (statically linked, no runtime deps)" + @echo " make linux Linux static build via Docker (canonical)" + @echo " make linux-local Linux static build locally (requires musl-gcc)" + @echo " make install Build + install to ~/.local/bin (auto-detects OS)" @echo " make verify-harden Verify binary hardening (stripped, no leaks)" @echo "" @echo "Native binary (requires local Chez + Jerboa):" new file mode 100755 --- /dev/null +++ b/build-gitsafe-macos.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# build-gitsafe-macos.sh — Build gitsafe as a maximally-static macOS binary +# +# Statically links: Chez kernel, lz4, zlib, ncurses +# Dynamically links: libSystem (libc/libpthread — always present on macOS), libiconv +# +# Prerequisites: +# - Chez Scheme installed (brew install chezscheme) +# - Jerboa libraries available +# - Homebrew ncurses (brew install ncurses) +# +# Produces: ./gitsafe-macos (single binary, no Chez runtime needed at run time) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Resolve jerboa +if [ -n "${JERBOA_HOME:-}" ]; then + JERBOA_LIB="${JERBOA_HOME}/lib" +elif [ -d "${SCRIPT_DIR}/../jerboa/lib" ]; then + JERBOA_LIB="$(realpath "${SCRIPT_DIR}/../jerboa/lib")" +elif [ -d "${HOME}/mine/jerboa/lib" ]; then + JERBOA_LIB="${HOME}/mine/jerboa/lib" +else + echo "ERROR: Cannot find Jerboa. Set JERBOA_HOME." + exit 1 +fi +export JERBOA_HOME="${JERBOA_HOME:-$(dirname "$JERBOA_LIB")}" + +echo "===================================" +echo "Building gitsafe-macos (static libs)" +echo "===================================" +echo "" +echo "Jerboa: $JERBOA_LIB" + +# Verify macOS +if [ "$(uname -s)" != "Darwin" ]; then + echo "ERROR: This build script is for macOS only." + exit 1 +fi + +# Check Chez +if ! command -v scheme &>/dev/null; then + echo "ERROR: scheme not found" + echo "Install: brew install chezscheme" + exit 1 +fi + +echo " scheme: $(command -v scheme) ($(scheme --version 2>&1))" +echo "" + +echo "[1/2] Validating macOS build environment..." + +# Check for ncurses static lib +NCURSES_A="" +for p in /opt/homebrew/opt/ncurses/lib /usr/local/opt/ncurses/lib; do + if [ -f "$p/libncurses.a" ]; then + NCURSES_A="$p/libncurses.a" + break + fi +done +if [ -z "$NCURSES_A" ]; then + echo "WARNING: static libncurses.a not found, will use dynamic -lncurses" + echo " Install: brew install ncurses" +fi +export NCURSES_STATIC_PATH="${NCURSES_A}" +echo " ncurses: ${NCURSES_A:-dynamic}" +echo "" + +echo "[2/2] Running macOS build..." +scheme -q --libdirs "${SCRIPT_DIR}:${JERBOA_LIB}" --script build-gitsafe-macos.ss + +# Verify +if [ -f "gitsafe-macos" ]; then + echo "" + echo "===================================" + echo "gitsafe-macos built successfully!" + echo "===================================" + ls -lh gitsafe-macos + echo "" + file gitsafe-macos + echo "" + echo "Dynamic dependencies:" + otool -L gitsafe-macos 2>/dev/null | tail -n +2 || true + echo "" + echo "Test: ./gitsafe-macos --version" +else + echo "ERROR: gitsafe-macos not created" + exit 1 +fi new file mode 100644 --- /dev/null +++ b/build-gitsafe-macos.ss @@ -0,0 +1,287 @@ +#!chezscheme +;; Build gitsafe as a maximally-static macOS binary. +;; +;; Usage: make macos +;; (runs via build-gitsafe-macos.sh -> this script) +;; +;; Statically links: Chez kernel, lz4, zlib, ncurses +;; Dynamically links: libSystem (always present), libiconv +;; +;; macOS does not support fully static binaries (Apple linker requires +;; libSystem.B.dylib), but this binary has zero third-party runtime deps. +;; +;; Produces: ./gitsafe-macos (single Mach-O binary, self-contained) + +(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))) + +(define chez-dir + (or (getenv "CHEZ_DIR") + (let ([mt (symbol->string (machine-type))] + [home (getenv "HOME")]) + (or (find-csv-dir (format "~a/.local/lib" home) mt) + (find-csv-dir "/opt/homebrew/lib" mt) + (find-csv-dir "/usr/local/lib" mt))))) + +(unless chez-dir + (display "Error: Cannot find Chez install dir. Set CHEZ_DIR.\n") + (exit 1)) + +;; --- Locate Jerboa --- +(define home (getenv "HOME")) +(define jerboa-dir + (or (getenv "JERBOA_HOME") + (let ([sibling (format "~a/../jerboa" (current-directory))]) + (and (file-exists? sibling) sibling)) + (begin + (display "Error: Cannot find Jerboa. Set JERBOA_HOME.\n") + (exit 1)))) + +(printf "=== gitsafe macOS build ===\n") +(printf "Chez dir: ~a\n" chez-dir) +(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"))) + +(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 --- +(printf "[4/6] Generating C main...\n") + +(call-with-output-file "gitsafe-main-macos.c" + (lambda (out) + (fprintf out "/* Auto-generated — do not edit */\n") + (fprintf out "#include <stdlib.h>\n") + (fprintf out "#include <stdio.h>\n") + (fprintf out "#include <string.h>\n") + (fprintf out "#include <unistd.h>\n") + (fprintf out "#include \"scheme.h\"\n") + (fprintf out "#include \"gitsafe_petite_boot.h\"\n") + (fprintf out "#include \"gitsafe_scheme_boot.h\"\n") + (fprintf out "#include \"gitsafe_boot.h\"\n") + (fprintf out "#include \"gitsafe_program.h\"\n") + (fprintf out "\n") + (fprintf out "int main(int argc, char *argv[]) {\n") + (fprintf out " char prog_path[256];\n") + (fprintf out " const char *tmpdir = getenv(\"TMPDIR\");\n") + (fprintf out " if (!tmpdir) tmpdir = \"/tmp\";\n") + (display " snprintf(prog_path, sizeof(prog_path), \"%s/gitsafe-XXXXXX\", tmpdir);\n" out) + (fprintf out " int fd = mkstemp(prog_path);\n") + (fprintf out " if (fd < 0) { perror(\"mkstemp\"); return 1; }\n") + (fprintf out " if (write(fd, gitsafe_program_data, gitsafe_program_size)\n") + (fprintf out " != (ssize_t)gitsafe_program_size) {\n") + (fprintf out " perror(\"write\"); close(fd); unlink(prog_path); return 1;\n") + (fprintf out " }\n") + (fprintf out " close(fd);\n") + (fprintf out "\n") + (fprintf out " Sscheme_init(NULL);\n") + (fprintf out " Sregister_boot_file_bytes(\"petite\", (void*)petite_boot_data, petite_boot_size);\n") + (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") + (fprintf out " int status = Sscheme_script(prog_path, argc, (const char **)argv);\n") + (fprintf out " unlink(prog_path);\n") + (fprintf out " Sscheme_deinit();\n") + (fprintf out " return status;\n") + (fprintf out "}\n")) + 'replace) + +;; --- Step 5: Compile and link (maximally static) --- +(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)) + (printf "Error: ~a not found at ~a\n" (car pair) (cdr pair)) + (exit 1))) + (list (cons "libkernel.a" kernel-a) + (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)) + (let ([p "/opt/homebrew/opt/ncurses/lib/libncurses.a"]) + (and (file-exists? p) p)) + (let ([p "/usr/local/opt/ncurses/lib/libncurses.a"]) + (and (file-exists? p) p)))) + +(printf " Static libs:\n") +(printf " kernel: ~a\n" kernel-a) +(printf " lz4: ~a\n" chez-lz4-a) +(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 + (if ncurses-a (string-append " " ncurses-a) ""))) + +(define dynamic-libs + (string-append + (if ncurses-a "" " -lncurses") + " -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) + +(printf "\nDone! Binary: ./gitsafe-macos\n") +(printf " Size: ") +(system "ls -lh gitsafe-macos | awk '{print $5}'") +(printf " SHA256: ") +(system "cat gitsafe-macos.sha256") +(printf "\n Test: ./gitsafe-macos --version\n") +(printf " Deps: otool -L gitsafe-macos\n")