jolt build: compile an app to a standalone binary (Phase 4 stages 1-2)

Restores the standalone-binary capability the Janet host had. `bin/joltc build
-m NS -o OUT` AOT-compiles an app into a single self-contained executable — the
whole runtime, clojure.core, stdlib and compiler embedded, no Chez install or
jolt source needed at runtime.

Pipeline (host/chez/build.ss, host primitive jolt.host/build-binary driven by
jolt.main's build command): resolve deps, load the entry namespace recording the
app namespaces in dependency order, re-emit each to Scheme, textually inline the
cli.ss runtime load sequence into one flat source + the app + a launcher, then
compile-file -> make-boot-file -> embed the boot as C bytes -> cc-link against
libkernel.a.

Two non-obvious bits: the compile pass runs in a fresh Chez, not the loaded
runtime (regex.ss shadows top-level `error`, which otherwise bakes a broken
reference into the boot); and the launcher installs scheme-start rather than
running -main at top level, since boot top-level forms execute during heap build
before argv is set, so args only reach -main through scheme-start.

Loader: a require of an in-memory namespace with no source file now no-ops, so
AOT'd app namespaces satisfy require in a built binary.

Mode flags (--opt/--dev, default release) are plumbed; the optimization passes
they gate come in a later stage. RFC 0007 has the design. Gated by `make
buildsmoke`.
This commit is contained in:
Yogthos 2026-06-22 23:01:36 -04:00
parent 33eff7c7d8
commit 43778eafd7
10 changed files with 554 additions and 12 deletions

View file

@ -112,6 +112,28 @@
(map? task) (do (apply-project! resolved) (apply-main-opts (:main-opts task) more))
:else (throw (ex-info (str "bad task " name) {})))))
;; build [-m NS | FILE] [-o OUT] [--opt | --dev] — AOT-compile the app into a
;; standalone executable. Resolves deps + roots like `run`, then hands the entry
;; namespace to the host build driver (jolt.host/build-binary, defined by
;; build.ss). Default mode is release; --opt selects optimized, --dev unoptimized.
(defn- cmd-build [more]
(apply-project! (deps/resolve-project (project-dir)))
(let [opts (loop [a more, entry nil, out nil]
(cond
(empty? a) {:entry entry :out out}
(= "-m" (first a)) (recur (drop 2 a) (second a) out)
(= "-o" (first a)) (recur (drop 2 a) entry (second a))
(str/starts-with? (first a) "-") (recur (rest a) entry out)
:else (recur (rest a) (or entry (first a)) out)))
entry (:entry opts)
mode (cond (some #{"--opt"} more) "optimized"
(some #{"--dev"} more) "dev"
:else "release")]
(when (nil? entry)
(throw (ex-info "build needs an entry: -m NS" {})))
(let [out (or (:out opts) (first (str/split entry #"\.")))]
(jolt.host/build-binary entry out mode))))
(defn- nrepl [more]
;; resolve the project (deps on the roots, native libs loaded), then start the
;; nREPL server so an editor can connect and (require '[some.lib]) live. A
@ -128,6 +150,7 @@
(println "usage: jolt <command> [args]")
(println " run -m NS [args] resolve deps.edn, load NS, call its -main")
(println " run FILE load a Clojure file")
(println " build -m NS [-o OUT] [--opt|--dev] compile a standalone binary")
(println " -M:alias [args] run the alias's :main-opts")
(println " -A:alias [args] add the alias's paths/deps")
(println " repl start a line REPL")
@ -146,4 +169,5 @@
(str/starts-with? cmd "-M") (cmd-M cmd more)
(str/starts-with? cmd "-A") (cmd-A cmd more)
(= cmd "-m") (cmd-run (cons "-m" more))
(= cmd "build") (cmd-build more)
:else (run-task cmd more))))