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 outputMeasured 2026-08-02 on the reference machine (Apple M5 Pro, go1.26.3): a hello binary is 7.1 MB stripped, starts in ~5 ms, peaks at 12.4 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.
Single-file mode
Section titled “Single-file mode”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 acore.clj(examples/hello/core.clj→hello). On Windows the default gets.exe; an explicit-ois honored verbatim, same rule asgo 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 generatedgo.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).
For a project that targets JVM Clojure and cljgo from one .cljc tree,
see Dual-host .cljc projects.
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.
Project mode
Section titled “Project mode”With no source-file argument, cljgo build [step] loads the project’s
build.cljgo (probed as build.cljgo > build.cljg),
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.
Cross-compilation — cljgo dist
Section titled “Cross-compilation — cljgo dist”Every cljgo binary is pure Go, CGO_ENABLED=0. That constraint has a
payoff: the Go toolchain cross-compiles to any platform from a single
host, with no C cross-toolchain, no sysroot, no per-OS CI runner.
cljgo dist (ADR 0077) turns that into one command:
$ cljgo distcljgo dist: building darwin/arm64 -> dist/myapp_darwin-arm64cljgo dist: building darwin/amd64 -> dist/myapp_darwin-amd64cljgo dist: building linux/amd64 -> dist/myapp_linux-amd64cljgo dist: building linux/arm64 -> dist/myapp_linux-arm64cljgo dist: building windows/amd64 -> dist/myapp_windows-amd64.exe
cljgo dist: 5 binaries in dist/The output directory is ready to attach to a GitHub Release or a Homebrew
tap: one native binary per platform plus a sha256sum -c-compatible
checksums.txt.
dist/ myapp_darwin-arm64 # Mach-O arm64 myapp_darwin-amd64 # Mach-O x86-64 myapp_linux-amd64 # ELF x86-64 myapp_linux-arm64 # ELF aarch64 myapp_windows-amd64.exe # PE32+ checksums.txtFlags:
- no flags — the five mainstream desktop/server targets above (Apple Silicon + Intel Mac, x86-64 + ARM Linux, Windows): effectively every real end user.
--target os/arch,…— an explicit subset, e.g.cljgo dist --target linux/amd64,windows/amd64. Each pair is validated againstgo tool dist list, so a typo is a named error, not a cryptic build failure.--all— everyGOOS/GOARCHthe toolchain supports (the long tail — freebsd, riscv64, wasm, … — opt-in, not in the default).-o dir— output directory (defaultdist).- a
<file.clj>positional — single-file input, same resolution ascljgo build; a bare invocation uses the project’sbuild.cljgoinstall artifact.
dist generates the (target-independent) Go module once and re-links
it per target, so the five-way build does not recompile your Clojure five
times. Since dist ships executables, a library project (no install
step) is a clear error pointing you at
cljgo publish.
This is a capability the JVM does not have: a .jar needs a JVM on the
target machine, and GraalVM native-image needs a builder per target
OS/arch. cljgo needs neither — which is one more reason the pure-Go
constraint stays sacred (a future cgo dependency would break dist).
The one caveat is inherited from Go: a
(go-require …)third-party module that itself uses cgo breaks cross-compilation. cljgo’s own runtime and the bri framework are pure Go, so the default path always cross-compiles.
Two things worth knowing
Section titled “Two things worth knowing”- 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 duringcljgo buildas 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.