October 24, 2019

Compiling Java Classes with Clojure Deps

In the past, I used Leinigen for Clojure projects. It downloads dependencies, compiles, handles the classpath, runs tests etc. Clojure 1.9 introduced the clj command line and the deps tooling. You can specify a deps.edn file and declare the dependencies in there. Deps not a build tool by design. it only downloads dependencies from Maven- and git repositories and builds the Java classpath. It doesn’t have other build features. However, for smaller Clojure projects that is enough and you can go without an extra build tool.

So, I started to use Deps as the starting point and it serves me well so far. Recently I wanted to add a Java class to my project. I do that when using Clojure-Java interop gets complex and it is just easier to have a few helper Java classes. However, how do I compile these classes? Leiningen has the lein javac command, but Deps doesn’t do builds.

Add Compiled Classes to Classpath

I started by compiling classes manually with IntelliJ IDEA. Unfortunately, when trying to use the class I got class not found exceptions.

user=> (blog_example.Test.)
Syntax error (ClassNotFoundException) compiling new at (REPL:1:1).
blog_example.Test

Well, the compiled Java classes need to be on the classpath build by Deps. You need to add the directory with the compiled classes to the :paths section in the deps.edn. I use the 'classes' directory which seems the Clojures default:

deps.clj:
{:paths   ["src" "classes"]
 :deps    {org.clojure/clojure             {:mvn/version "1.10.1"}
 ...}

After that, clj does find the compiled classes.

:aliases, Secret Weapon of clj and deps.edn

Deps has an :aliases section. Aliases allow to create different classpaths for different environments, like support for different Clojure/Clojurescript versions, test environments etc. It also allows to change the default start command for that alias. That combined you can create your small commands for a project:

:aliases {
       ; Test environment
       :test     {:extra-paths ["test"]}
       ; Find outdated dependencies: clojure -Aoutdated -a outdated
       :outdated {:extra-deps {olical/depot {:mvn/version "1.8.4"}}
                  :main-opts  ["-m" "depot.outdated.main" "-a" "outdated"]}
}

So, we can create a command to compile our Java class. I remembered the standard Java API to compile [https://docs.oracle.com/javase/8/docs/api/javax/tools/JavaCompiler.html]Java, but I wanted something which only needs a few lines. Or maybe I can invoke the 'javac' compiler myself. Anyway, the solution needs to be small, otherwise, I switch to Leinigen

Badigeon Library: Compile stuff

My search leads to the Badigeon library, which exactly gives you simple compile commands.

So, I created a build/build.clj file with a simple main function, which invokes the Java compiler:

build/build.clj:
(ns build
  (:require [badigeon.javac :as j]))

(defn javac []
  (println "Compiling Java")
  (j/javac "src" {:compile-path     "classes"
                  ;; Additional options used by the javac command
                  :compiler-options ["-cp" "src:classes" "-target" "1.8"
                                     "-source" "1.8" "-Xlint:-options"]})
  (println "Compilation Completed"))

(defn -main []
  (javac))

And in the deps.edn file I added a :javac alias, which invokes this script.

deps.edn:
 :aliases {:javac {:extra-paths ["build"]
                   :extra-deps  {badigeon/badigeon {:git/url "https://github.com/EwenG/badigeon.git"
                                                    :sha     "c7588e6d2c66284dcda1a339adcba8cb9c74a8b0"
                                                    :tag     "0.0.9"}}
                   :main-opts   ["-m" "build"]}}

And then I invoke the compile step with:

gamlor@gamlor-t470p ~/h/example> clj -Ajavac
Compiling Java
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Compilation Completed

That’s good enough for me right now. As the project matures I probably will create a more extended build or switch to Leinigen if I need more.

clj and deps caring about Java classes

More Deps Tool

Also, check out this list of Deps tools: https://github.com/clojure/tools.deps.alpha/wiki/Tools

Tags: Clojure Java