This post will detail the steps to setup Clojure and GraalVM to generate native executable. I used a similar approach when creating my project cljcc. It has a few extra steps on on top of generating a native image, but this post will have just the minimum things required to build uberjar and generating image.
Requirements #
Project Structure #
├── build.clj
├── deps.edn
├── src
│ └── demo
│ └── core.clj
└── target
The demo/core.clj
file simply prepends "Hello"
to the first argument, and prints to stdout.
(ns demo.core
(:gen-class))
(defn -main [& args]
(println (str "Hello " (first args))))
The :gen-class
is necessary, as this informs the build process to generate a Main entrypoint for the Java program. Without :gen-class
, although a uberjar will be built,
the native image generation will fail with the below error.
Error: Main entry point class 'demo.core' neither found on
classpath: '/home/shagun-agrawal/Development/setup-clj-graalvm/target/demo.core-1.0.0-standalone.jar' nor
modulepath: '/home/shagun-agrawal/.sdkman/candidates/java/23-graalce/lib/svm/library-support.jar'.
Internal exception: com.oracle.svm.core.util.UserError$UserException: Main entry point class 'demo.core' neither found on
classpath: '/home/shagun-agrawal/Development/setup-clj-graalvm/target/demo.core-1.0.0-standalone.jar' nor
modulepath: '/home/shagun-agrawal/.sdkman/candidates/java/23-graalce/lib/svm/library-support.jar'.
at org.graalvm.nativeimage.builder/com.oracle.svm.core.util.UserError.abort(UserError.java:85)
at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.buildImage(NativeImageGeneratorRunner.java:440)
at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.build(NativeImageGeneratorRunner.java:711)
at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.start(NativeImageGeneratorRunner.java:139)
at org.graalvm.nativeimage.builder/com.oracle.svm.hosted.NativeImageGeneratorRunner.main(NativeImageGeneratorRunner.java:94)
Below is the deps.edn
file. It has an extra alias for nrepl.
{:paths ["src"]
:deps {com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}}
:aliases
{:run-main {:main-opts ["-m" "demo.core"]}
:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
:ns-default build}
:nrepl {:extra-deps {nrepl/nrepl {:mvn/version "1.3.0"}
cider/cider-nrepl {:mvn/version "0.50.2"}
refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}}
:main-opts ["-m" "nrepl.cmdline" "--interactive" "--color" "--middleware" "[cider.nrepl/cider-middleware,refactor-nrepl.middleware/wrap-refactor]"]}}}
Adding aliases to deps.edn
can be managed using a tool called neil.
I found about this tool from Developer Tooling for Speed and Productivity in 2024 | Vedang Manerikar.
I earlier used to rely on Doom Emacs cider-jack-in-clj
function, which starts a clojure REPL automatically and connects to it, but I wasn’t aware of how it works ( for e.g. the command lines options being passed etc ).
Starting up a repl in different shell and connecting to it from my editor is much simpler.
It also makes it editor agnostic, as the setup for starting a REPL is present in the deps file itself
The above video also includes setup for logging, flowstorm debugger, documentation, project structure etc.
Use clj -M:nrepl
to start the server. I then use Doom Emacs cider-connect
function to attach to it.
Building Uberjar #
build.clj
(ns build
(:require [clojure.tools.build.api :as b]))
(def lib 'demo.core)
(def version "1.0.0")
(def class-dir "target/classes")
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))
;; delay to defer side effects (artifact downloads)
(def basis (delay (b/create-basis {:project "deps.edn"})))
(defn clean [_]
(b/delete {:path "target"}))
(defn uber [_]
(clean nil)
(b/copy-dir {:src-dirs ["src" "resources"]
:target-dir class-dir})
(b/compile-clj {:basis @basis
:ns-compile '[demo.core]
:class-dir class-dir})
(b/uber {:class-dir class-dir
:uber-file uber-file
:basis @basis
:main 'demo.core}))
Run clj -T:build uber
, which generates a jar file under /target
directory.
java -jar ./target/demo.core-1.0.0-standalone.jar World
Hello World
To automate the creation of a new application, take a look at deps-new. It automatically sets up a deps.edn
and build.clj
file. and has aliases for starting repl, building uberjar etc.
The deps.edn
file also has a dependency on graal-build-time. Adding it to deps adds this library to classpath.
clj -Spath # without adding library in deps.edn
src:/home/shagun-agrawal/.m2/repository/org/clojure/clojure/1.12.0/clojure-1.12.0.jar:/home/shagun-agrawal/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar:/home/shagun-agrawal/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar
clj -Spath # after adding library in deps.edn
src:/home/shagun-agrawal/.m2/repository/com/github/clj-easy/graal-build-time/1.0.5/graal-build-time-1.0.5.jar:/home/shagun-agrawal/.m2/repository/org/clojure/clojure/1.12.0/clojure-1.12.0.jar:/home/shagun-agrawal/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar:/home/shagun-agrawal/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar
Without this dependency, the generated jar after the build also has differences.
jar tf demo.core-1.0.0-standalone.jar | grep '/' | cut -d'/' -f1 | sort -u # without dep
clojure
demo
META-INF
jar tf demo.core-1.0.0-standalone.jar | grep '/' | cut -d'/' -f1 | sort -u # with dep
clj-easy
clj_easy
clojure
demo
META-INF
The native image commands needs to initialize .class
files at build time. To automatically identify which files needs to be initialized,
graal-build-time
library will detect .class
files, and uses the feature flag ( mentioned in the below command ) to mark them to be initialized at build time.
Generate Native Image #
native-image -jar target/demo.core-1.0.0-standalone.jar -o target/demo -H:+ReportExceptionStackTraces --features=clj_easy.graal_build_time.InitClojureClasses --report-unsupported-elements-at-runtime --verbose --no-fallback
This generates an executable at /target/demo
.
./target/demo "World"
Hello World
Alias for adding Flowstorm #
I couldn’t find the command in neil which adds flowstorm alias to a project. The below alias will setup nREPL and flowstorm.
:storm {;; for disabling the official compiler
:classpath-overrides {org.clojure/clojure nil}
:extra-deps {io.github.clojure/tools.build {:mvn/version "0.10.3"}
nrepl/nrepl {:mvn/version "1.3.0"}
cider/cider-nrepl {:mvn/version "0.50.2"}
refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}
com.github.flow-storm/clojure {:mvn/version "1.11.4-1"}
com.github.flow-storm/flow-storm-dbg {:mvn/version "4.0.1"}}
:jvm-opts ["-Dclojure.storm.instrumentEnable=true"
"-Dclojure.storm.instrumentOnlyPrefixes=<NAMESPACE>"]
:main-opts ["-m" "nrepl.cmdline" "--interactive" "--color" "--middleware" "[flow-storm.nrepl.middleware/wrap-flow-storm,cider.nrepl/cider-middleware,refactor-nrepl.middleware/wrap-refactor]"]}
Use clj -M:storm
to start a nREPL session with flowstorm. Evaluate :dbg
in the REPL to launch the debugger.
Refer Flowstorm Documentation on how to use the debugger.