Skip to content

Compile & ship binaries

cljgo build is the ClojureScript model with Go as the JavaScript: the compiler emits plain Go source — one Clojure namespace becomes one Go package — then invokes go build on the generated module. The result is a single static native binary. No JVM, no runtime install, nothing to ship but the file.

$ cljgo run hello.clj # interpreted
$ cljgo build hello.clj # -> ./hello, a static native binary
$ ./hello # byte-identical output

Measured on the reference machine (Apple M5 Pro, go1.26.3): a hello binary is 5.3 MB stripped, starts in ~5 ms, peaks at 14.7 MB RSS. Numbers, methodology, and head-to-head comparisons are on the benchmarks page.

cljgo build is the one command that needs the Go toolchain on PATH (cljgo run and cljgo repl do not — see Install). Release binaries pin the published runtime module in the generated go.mod; the first build fetches it from the Go module proxy once per machine.

cljgo build [-o out] [-gen dir] [-runtime dir] <file.clj>
  • -o out — output binary path. Default: the file’s base name, or the parent directory name for a core.clj (examples/hello/core.cljhello). On Windows the default gets .exe; an explicit -o is honored verbatim, same rule as go build -o.
  • -gen dir — keep the generated Go module there instead of a temp dir. The output is honest, gofmt-ed Go source you can read; a // Code generated by cljgo build. DO NOT EDIT. header marks each file.
  • -runtime dir — the cljgo source tree used for the generated go.mod’s replace directive (defaults to $CLJGO_SRC; release binaries pin the published module instead).

Source files may be .clj, .cljc, .cljg, or .cljgo (ADR 0055).

The build strips by default (-trimpath -ldflags="-s -w"), and a compiled binary links the compiled clojure.core — never the interpreter. The tree-walk evaluator contributes zero symbols to the link set, CI-enforced (ADR 0046); that is why a compiled binary starts in ~5 ms instead of the interpreter’s ~32 ms boot.

With no source-file argument, cljgo build [step] loads the project’s build.cljgo (probed as build.cljgo > build.cljg > build.clj), evaluates its (defn build [b] …) plan, and runs the requested step — the zig-build model (ADR 0021):

;; build.cljgo — what `cljgo new --template cli` generates
(defn build [b]
(let [app (exe b {:name "newapp"
:main "src/newapp/core.cljg"})]
(install b app)
(run b app)))
$ cljgo build # default step: install ./newapp
$ cljgo build run # build it and run it (mirrors `zig build run`)

Project builds also resolve declared dependencies and write/verify build.lock.edn — see Dependencies & publishing. Third-party Go modules declared with (go-require …) are fetched and linked into the binary here.

Because the output is plain Go, the standard Go cross-compilation environment variables work directly:

$ GOOS=linux GOARCH=amd64 cljgo build -o hello-linux hello.clj
$ file hello-linux
hello-linux: ELF 64-bit LSB executable, x86-64, statically linked, stripped

(Verified from macOS/arm64.) All the usual Go targets apply — macOS/Linux/Windows, amd64/arm64. The usual Go caveat applies too: cgo-based dependencies generally break cross-compiles (you need a target C toolchain and sysroot). A first-class per-artifact :target surface in build.cljgo is planned but not wired yet.

  • Top-level forms are evaluated at compile time. The emitter discovers namespaces by evaluating require forms through the interpreter, so a bare top-level (println …) in your source prints during cljgo build as well as at runtime. Put program behavior in -main.
  • Compiled output is byte-identical to the interpreter. The dual-harness conformance suite (416 oracle-cited files) runs every test through both paths on every commit; a REPL-vs-binary divergence is treated as a release blocker. See the REPL guide and architecture.

Compiled code is also where the performance work lands: chunk-aware sequence ops, direct-call emission, and int64 numeric inference (ADRs 0063–0067) put emitted hot loops at ~5× handwritten Go — details on the benchmarks page.

Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.