Setup Clojure with GraalVM for Native Image
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
└── targetThe demo/core.clj file simply prepends "Hello" to the first argument, and prints to stdout.
1(ns demo.core
2 (:gen-class))
3
4(defn -main [& args]
5 (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.
1{:paths ["src"]
2 :deps {com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}}
3 :aliases
4 {:run-main {:main-opts ["-m" "demo.core"]}
5 :build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.6" :git/sha "52cf7d6"}}
6 :ns-default build}
7 :nrepl {:extra-deps {nrepl/nrepl {:mvn/version "1.3.0"}
8 cider/cider-nrepl {:mvn/version "0.50.2"}
9 refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}}
10 :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
1(ns build
2 (:require [clojure.tools.build.api :as b]))
3
4(def lib 'demo.core)
5(def version "1.0.0")
6(def class-dir "target/classes")
7(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))
8
9;; delay to defer side effects (artifact downloads)
10(def basis (delay (b/create-basis {:project "deps.edn"})))
11
12(defn clean [_]
13 (b/delete {:path "target"}))
14
15(defn uber [_]
16 (clean nil)
17 (b/copy-dir {:src-dirs ["src" "resources"]
18 :target-dir class-dir})
19 (b/compile-clj {:basis @basis
20 :ns-compile '[demo.core]
21 :class-dir class-dir})
22 (b/uber {:class-dir class-dir
23 :uber-file uber-file
24 :basis @basis
25 :main 'demo.core}))Run clj -T:build uber, which generates a jar file under /target directory.
1java -jar ./target/demo.core-1.0.0-standalone.jar World
2
3Hello WorldTo 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.
1clj -Spath # without adding library in deps.edn
2src:/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
3
4clj -Spath # after adding library in deps.edn
5src:/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.jarWithout this dependency, the generated jar after the build also has differences.
1jar tf demo.core-1.0.0-standalone.jar | grep '/' | cut -d'/' -f1 | sort -u # without dep
2clojure
3demo
4META-INF
5
6jar tf demo.core-1.0.0-standalone.jar | grep '/' | cut -d'/' -f1 | sort -u # with dep
7clj-easy
8clj_easy
9clojure
10demo
11META-INFThe 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
1native-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-fallbackThis generates an executable at /target/demo.
1./target/demo "World"
2
3Hello WorldAlias 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.
1:storm {;; for disabling the official compiler
2 :classpath-overrides {org.clojure/clojure nil}
3 :extra-deps {io.github.clojure/tools.build {:mvn/version "0.10.3"}
4 nrepl/nrepl {:mvn/version "1.3.0"}
5 cider/cider-nrepl {:mvn/version "0.50.2"}
6 refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"}
7 com.github.flow-storm/clojure {:mvn/version "1.11.4-1"}
8 com.github.flow-storm/flow-storm-dbg {:mvn/version "4.0.1"}}
9 :jvm-opts ["-Dclojure.storm.instrumentEnable=true"
10 "-Dclojure.storm.instrumentOnlyPrefixes=<NAMESPACE>"]
11 :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.