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! :^)