With tools.deps we can now build libraries and publish them to our favourite Git hosting provider and have others consume them as dependencies without the need to package and publish them as JARs. As we’ve seen previously, we can do this using the tools provided by Clojure and the JVM. However, these do not integrate easily with tools.deps and there are various common tasks we’d like to automate. Clojure provides the tools.build library for defining build tasks.

After showing your CTO the Clojure greet application, she’s very impressed but informs you the company has recently invested heavily in spare { and } keys, so all library development must be done in Java. The application itself can still be written in Clojure.

You restructure your project to have separate source directories for Java and Clojure:

greeter/
  deps.edn
  build.clj
  java/
    src/
  clojure/
    src/

Then define the Java library:

java/src/com/picosoft/greet/Greeter.java

package com.picosoft.greet;

public class Greeter {
    public void greet(String who, boolean excite) {
	String message = excite ?
	    String.format("Hello %s!", who) :
	    String.format("Hello %s", who);
	System.out.println(message);
    }
}

The main Clojure namespace imports this class for the greet implementation

clojure/src/greet/main.clj

(ns greet.main
  (:gen-class)
  (:require [clojure.tools.cli :as cli])
  (:import [com.picosoft.greet Greeter]))

(def cli-options [["-x" "--excite" "Show some excitement"
                   :default false]])

(defn- parse-args [args]
  (let [{:keys [options arguments errors]} (cli/parse-opts args cli-options)]
    (if (seq errors)
      (binding [*out* *err*]
        (doseq [err errors]
          (println err))
        (System/exit 1))
      {:who (or (first arguments) "world")
       :excite (:excite options)})))

(defn -main [& args]
  (let [{:keys [who excite]} (parse-args args)
        g (Greeter.)]
    (.greet g who excite)))

The Clojure source location needs to be specified in the deps.edn file along with the tools.cli dependency. There is also a build alias used by our build definition.

deps.edn

{:paths ["clojure/src"]
 :deps {org.clojure/tools.cli {:mvn/version "1.0.219"}}
 :aliases {:build {:deps {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
                   :ns-default build}}
 }

Finally our tools.build script defines an uber task which compiles the Java and Clojure code and packages it into an uberjar.

build.clj

(ns build
  (:require [clojure.tools.build.api :as b]))

(defn uber [_]
  (let [basis (b/create-basis)
        output-dir "target/uber"]
    (b/javac {:basis basis
              :src-dirs ["java/src"]
              :class-dir output-dir})
    (b/compile-clj {:basis basis
                    :class-dir output-dir
                    :src-dirs ["clojure/src"]})
    (b/uber {:uber-file "target/greeter-standalone.jar"
             :class-dir output-dir
             :basis basis
             :main 'greet.main})))

(defn clean [_]
  (b/delete {:path "target"}))

This task can be invoked with

> clojure -T:build uber

this builds the uberjar at target/greeter-standalone.jar which can be run with

> java -jar target/greeter-standalone.jar --excite everyone
Hello everyone!