GraalVM is an Oracle project which allows building dynamic ELFs (native executables) on Linux. This has the advantage of being way smaller (no JDK to ship), easier to forward arguments, and way faster to start! This may be less optimized than using the JIT of the JDK, but my needs are way beyond that anyway.
Building an uberjar
There are several ways of doing it, currently I'm using the following.
I use deps.edn
to manage dependencies, and I have an alias that allows me to
build an uberjar :
{:src ["src"]
:aliases {:uberjar} {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}}
:main-opts [ "-m" "hf.depstar.uberjar"]}}
And I call it the following way :
clojure -Scp $(clj -Spath) \
-M:uberjar \
program.jar \
-C -m program.core
This builds a jar that should contain everything that's needed.
New dependency needed!
From GraalVM 22, the argument --initialize-at-build-time
has been removed,
which breaks all clojures binaries since clojure/core__init.class relies on it
(apparently).
After some testing and googling, I found the following fix here.
You simply need to add the following dependency to your deps.edn
and it will
fix it automatically! Hurah!
{:deps {com.github.clj-easy/graal-build-time {:mvn/version "0.1.4"}}}
You'll need to run clj -Spath
as well as uberjar again ;).
GraalVM native-image
We need to generate some reflection config (especially since Clojure is highly dynamic).
java -agentlib:native-image-agent=config-output-dir=reflect-config -jar program.jar
This builds some metadata that we will use to generate the native binary.
To build a native-image, it's apparently best to go from an uberjar (hence the previous part).
native-image \
-cp $(clj -Spath) \
-jar program.jar \
program \
--verbose \
-H:+ReportExceptionStackTraces \
-H:ConfigurationFileDirectories=reflect-config
You can pass an additional --no-fallback
if you absolutely don't want a JDK
included because that's what it fallbacks to.
Nix based build
Since I'm using Nix, I integrated my custom software in my system configuration using Nix. This makes it more easily repeatable which is very nice!
I use the following default.nix
:
It uses gitignoreSource from https://github.com/hercules-ci/gitignore.nix
let
inherit (repo) gitignoreSource;
inherit (repo.lib) concatStringsSep;
inherit (myLib)
mkJar
mkNativeFromJar
;
deps = import ./deps.nix {
inherit (pkgs) fetchMavenArtifact fetchgit lib;
};
depsPaths = deps.makePaths { };
# NB: this is useful if static assets are used and need to be packaged inside
# the jar.
#resources = builtins.filterSource (_: type: type != "symlink") ./resources;
classpath.prod = concatStringsSep ":" (
(map gitignoreSource [ ./src ./test ]) ++ depsPaths # [ resources ] ++
);
mainClass = "program.core";
src = ./.;
in rec {
jar = mkJar {
name = "program.jar";
inherit
mainClass
classpath
src
;
};
bin = mkNativeFromJar {
name = "program";
entryJar = jar;
reflectionDir = ./reflect-config;
inherit
classpath
;
};
# For dev purposes and not build dev
shell = myLib.mkShell {
packages = with pkgs; [ hello ];
shellHook = ''
echo Test!
echo Hi!
'';
};
}
The nix configuration for the project is generated by using the clj2nix
project :
clj2nix deps.edn deps.nix -A:uberjar
This needs the -A:uberjar
since it won't include the depstar dependency
otherwise.
The mkJar
/ mkNativeFromJar
work the following way :
{
# Clojure build functions
## We need the uberjar alias to create this dependencies (as the presence of it
## inside generated deps.nix). Deps.nix is generated with clj2nix w/ uberjar
## alias.
mkJar = {
name,
mainClass,
classpath,
src
}: pkgs.stdenv.mkDerivation rec {
inherit name;
dontUnpack = true;
buildPhase = ''
export HOME=$(pwd)
cp -rf ${src}/* .
${pkgs.clojure}/bin/clojure \
-Scp ${classpath.prod} \
-M:uberjar \
${name} \
-C -m ${mainClass}
'';
doCheck = true;
checkPhase = ''
echo "checking for existence of ${name}"
[ -f ${name} ]
'';
installPhase = ''
cp ${name} $out
'';
};
## Create native binary w/ GraalVM. Note that this is expensive CPU-wise and
## quite slow if you build it everytime. Startup latency of those binaries is
## lower though.
## Enterprise GraalVM has more features but it's currently not in Nixpkgs.
mkNativeFromJar = {
name,
entryJar,
reflectionDir,
classpath,
# Check that binary runs
doCheck ? true,
# By default, don't generate an image that is not AOTed
# If a class is missing at runtime, will fail
noFallback ? true
}: pkgs.stdenv.mkDerivation rec {
inherit name;
dontUnpack = true;
# ReportExceptionStackTraces : to get an idea of where reflection is used
# no-fallback : don't build a slow image is reflection is used
# Reflection configuration contains class to load that use reflection
buildPhase = ''
${pkgs.graalvm17-ce}/bin/native-image \
-cp ${classpath.prod} \
-jar ${entryJar} \
${name} \
--verbose \
-H:+ReportExceptionStackTraces \
-H:ConfigurationFileDirectories=${reflectionDir} \
${if noFallback then "--no-fallback" else ""}
'';
inherit doCheck;
checkPhase = ''
echo "checking for existence of ${name}"
[ -f ${name} ]
./${name}
'';
installPhase = ''
cp ${name} $out
'';
};
}
And that's all! :^)