Clojure

tools.build Guide

tools.build is a library of functions for building Clojure projects. They are intended to be used in a build program to create user-invokable target functions.

The git dep and Clojure CLI examples in this guide assume the use of Clojure CLI 1.10.3.933 or higher. It is possible to use tools.build with earlier versions of deps.edn/Clojure CLI but the deps.edn git coordinates and Clojure CLI commands will vary.

Source library jar build

The most common Clojure build creates a jar file containing Clojure source code. To do this with tools.build we’ll use the following tasks:

  • create-basis - to create a project basis

  • copy-dir - to copy Clojure source and resources into a working dir

  • write-pom - to write a pom file in the working dir

  • jar - to jar up the working dir into a jar file

The build.clj will look like this:

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

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def jar-file (format "target/%s-%s.jar" (name lib) version))

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

(defn jar [_]
  (b/write-pom {:class-dir class-dir
                :lib lib
                :version version
                :basis basis
                :src-dirs ["src"]})
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir})
  (b/jar {:class-dir class-dir
          :jar-file jar-file}))

Some things to notice:

  • This is just normal Clojure code - you can load this namespace in your editor and develop it interactively at the REPL.

  • As a single-purpose program, it’s fine to build shared data in the set of vars at the top.

  • We are choosing to build in the "target" directory and assemble the jar contents in "target/classes" but there is nothing special about these paths - it is fully in your control. Also, we’ve repeated those paths and others in multiple places here but you can remove that duplication to the extent that feels right.

  • We’ve used the tools.build task functions to assemble larger functions like build/jar for the user to invoke. These functions take a parameter map and we’ve chosen not to provide any configurable parameters here, but you could!

The deps.edn file will look like this:

{:paths ["src"]
 :aliases
 {:build {:deps {io.github.clojure/tools.build {:tag "TAG" :sha "SHA"}}
          :ns-default build}}}

And then you can run this build with:

clj -T:build clean
clj -T:build jar

We expect to be able to do these both together on the command line but that is a work in progress.

Compiled uberjar application build

When preparing an application, it is common to compile the full app + libs and assemble the entire thing as a single uberjar. An example build for this might look like this:

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

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))

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

(defn prep [_]
  (b/write-pom {:class-dir class-dir
                :lib lib
                :version version
                :basis basis
                :src-dirs ["src"]})
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir}))

(defn uber [_]
  (b/compile-clj {:basis basis
                  :src-dirs ["src"]
                  :class-dir class-dir})
  (b/uber {:class-dir class-dir
           :uber-file uber-file
           :basis basis}))

The deps.edn and build execution will look the same as the prior example.

Invocation now includes three functions (you may not need to call all 3):

clj -T:build clean
clj -T:build prep
clj -T:build uber

Rather saying all of these each time, you might consider making a composite function in build.clj:

(defn all [_]
  (do (clean nil) (prep nil) (uber nil)))
clj -T:build all

Parameterized builds

In the builds above we did not parameterize any aspect of the build, just chose which functions to call. You may find that it’s useful to parameterize your builds to differentiate dev/qa/prod, or version, or some other factor. To account for function chaining at the command line, it is advisable to establish the common set of parameters to use across your build functions and have each function pass the parameters along.

For example, consider a parameterization that includes an extra set of dev resources to set a local developer environment. We’ll use a simple :env :dev kv pair to indicate this:

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

(def lib 'my/lib1)
(def version (format "1.2.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def jar-file (format "target/%s-%s.jar" (name lib) version))
(def copy-srcs ["src" "resources"])

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

(defn jar [{:keys [env] :as params}]
  (let [srcs (if (= env :dev) (cons "dev-resources" copy-srcs) copy-srcs)]
    (b/write-pom {:class-dir class-dir
                  :lib lib
                  :version version
                  :basis basis
                  :src-dirs ["src"]})
    (b/copy-dir {:src-dirs srcs
                 :target-dir class-dir})
    (b/jar {:class-dir class-dir
            :jar-file jar-file})
    params))

The other aspects of deps.edn and invocation remain the same.

Invocation that activates :dev environment will look like this:

clj -T:build jar :env :dev

The kv params are passed to the jar function.

Build tasks

Currently, tools.build comes packaged with the following tasks (see the API for details):

Table 1. Build Tasks
Domain Function Description Required Params Optional Params

File

delete

Delete file or directory recursively, if it exists.

:path

File

copy-file

Copy one file from source to target, creating target directories if needed.

:src, :target

File

copy-dir

Copy the contents of the :src-dirs to the :target-dir, optionally perform text replacement.

:src-dirs, :target-dir

:include, :replace

File

write-file

Like clojure.core/spit, but create directories if needed.

:path

:content, :opts

Compilation

javac

Compile Java source to classes.

:src-dirs, :class-dir

:basis, :javac-opts

Compilation

compile-clj

Compile Clojure source to classes.

:basis, :src-dirs, :class-dir

:compile-opts, :ns-compile, :filter-nses

Artifact

jar

Create a jar file.

:class-dir, :jar-file

:main

Artifact

uber

Create an uberjar file.

:class-dir, :uber-file

:basis, :main

Artifact

zip

Create a zip file.

:src-dirs, :zip-file

Process

java-command

Create command line args for a Java process from a basis.

:basis, :main

:java-cmd, :java-opts, :main-args

Process

process

Execute an external command.

:command-args

:dir, :out, :err, :out-file, :err-file, :env

Maven

write-pom

Write a pom file to class-dir, either by updating an existing POM or generating a new one from deps.edn

:basis, :class-dir

:src-pom, :lib, :version, :src-dirs, :resource-dirs, :repos

Maven

install

Install Maven jar to local repo.

:basis, :lib

:classifier, :jar-file, :class-dir

Original author: Alex Miller