diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09f615a..4668e20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,22 @@ jobs: out="$(./target/release/joltc -e '(reduce + (range 10))')" test "$out" = "45" || { echo "joltc -e gave '$out', want 45"; exit 1; } + # The binary is a self-contained COMPILER: it must `build` an app with no + # jolt source on disk. Run from an isolated dir (nothing but the tiny app) + # so a build that reaches for host/chez/*.ss on the filesystem fails here, + # not on a user's machine. + - name: Smoke a self-contained build + run: | + joltc="$(pwd)/target/release/joltc" + work="$(mktemp -d)" + mkdir -p "$work/app/src/app" + printf '{:paths ["src"]}\n' > "$work/app/deps.edn" + printf '(ns app.core)\n(defn -main [& _] (println "built:" (reduce + (range 10))))\n' \ + > "$work/app/src/app/core.clj" + ( cd "$work/app" && "$joltc" build -m app.core -o app ) + out="$("$work/app/app")" + test "$out" = "built: 45" || { echo "self-contained build ran '$out', want 'built: 45'"; exit 1; } + - name: Package run: | ver="${GITHUB_REF_NAME}" diff --git a/host/chez/build-joltc.ss b/host/chez/build-joltc.ss index 1574588..b1f3a8b 100644 --- a/host/chez/build-joltc.ss +++ b/host/chez/build-joltc.ss @@ -80,6 +80,32 @@ (bld-walk-files root "" '()))) (list "jolt-core" "stdlib"))) +;; Embed every runtime .ss the build inlines into an app (the transitive closure of +;; the manifest's loads: rt.ss + all it loads, the seed, compile-eval, loader, ffi, +;; png, vendored irregex). Keyed by the exact path the (load "…") forms use, so +;; build.ss's bld-source-string reads them from the binary with no jolt source on +;; disk. Traversal mirrors bld-emit-runtime/bld-inline-line via the same +;; bld-file-lines + bld-load-path, so the embedded set is exactly what build reads. +(define (jb-collect-load-paths) + (let ((seen (make-hashtable string-hash string=?)) (order '())) + (define (walk path) + (when (and path (not (hashtable-ref seen path #f))) + (hashtable-set! seen path #t) + (set! order (cons path order)) + (for-each (lambda (l) (walk (bld-load-path l))) (bld-file-lines path)))) + (for-each (lambda (entry) (when (string? entry) (walk (bld-load-path entry)))) + bld-runtime-manifest) + (for-each (lambda (kv) (walk (bld-load-path (cdr kv)))) bld-tagged-loads) + (reverse order))) + +(define (jb-emit-runtime-embeds out) + (for-each + (lambda (path) + (put-string out (string-append + "(register-embedded-resource! " (ei-str-lit path) " " + (ei-str-lit (read-file-string path)) ")\n"))) + (jb-collect-load-paths))) + ;; The launcher (Chez scheme-start): replicates host/chez/cli.ss but reads argv ;; from the scheme-start lambda and has no repo root to cd into (all source is ;; embedded; JOLT_PWD defaults to cwd via io/jolt.main). build.ss is already @@ -134,6 +160,8 @@ (bld-emit-runtime out #f #f) (put-string out "\n;; === build driver (inlined for self-contained `jolt build`) ===\n") (bld-inline-line "(load \"host/chez/build.ss\")" out 0) + (put-string out "\n;; === embedded runtime source (self-contained `build` reads these) ===\n") + (jb-emit-runtime-embeds out) (put-string out "\n;; === embedded jolt-core + stdlib source ===\n") (jb-emit-source-embeds out) (put-string out "\n;; === joltc launcher ===\n") diff --git a/host/chez/build.ss b/host/chez/build.ss index d4564a2..f1bdd0c 100644 --- a/host/chez/build.ss +++ b/host/chez/build.ss @@ -141,12 +141,23 @@ (q2 (let scan ((i (+ q1 1))) (if (char=? (string-ref s i) #\") i (scan (+ i 1)))))) (substring s (+ q1 1) q2))))) -(define (bld-file-lines path) - (call-with-input-file path - (lambda (p) - (let loop ((acc '())) - (let ((l (get-line p))) - (if (eof-object? l) (reverse acc) (loop (cons l acc)))))))) +;; runtime source for PATH: from the binary's embedded store if present (a +;; self-contained joltc building an app, with no jolt checkout on disk), else read +;; from disk (running from a source checkout). build-joltc embeds every runtime +;; .ss the manifest inlines, so `build` never touches the filesystem for them. +(define (bld-source-string path) + (let ((emb (hashtable-ref embedded-resources path #f))) + (if (string? emb) emb (read-file-string path)))) + +(define (bld-string-lines s) + (let ((n (string-length s))) + (let loop ((i 0) (start 0) (acc '())) + (cond ((>= i n) (reverse (if (> i start) (cons (substring s start i) acc) acc))) + ((char=? (string-ref s i) #\newline) + (loop (+ i 1) (+ i 1) (cons (substring s start i) acc))) + (else (loop (+ i 1) start acc)))))) + +(define (bld-file-lines path) (bld-string-lines (bld-source-string path))) ;; Emit one line to OUT, recursively inlining a `(load ...)` of a repo file. (define (bld-inline-line line out depth) diff --git a/host/chez/dce.ss b/host/chez/dce.ss index 19a6eef..b7d9d06 100644 --- a/host/chez/dce.ss +++ b/host/chez/dce.ss @@ -90,7 +90,10 @@ ;; str re-serializes the read form (compiled identically; comments/whitespace are ;; irrelevant). (define (dce-blob-records path) - (call-with-input-file path + ;; bld-source-string (build.ss) reads the embedded copy when running from a + ;; self-contained joltc, else the file on disk — so tree-shake works with no + ;; jolt checkout present. Forward ref: build.ss loads after this file. + (call-with-port (open-input-string (bld-source-string path)) (lambda (p) (let loop ((acc '())) (let ((form (read p)))