Packaging Clojure projects into jars and uberjars with tools.build
Tags: clojure, cli, build
Published: 2022-06-03 20:22:00


This article was originally posted at [kozieiev.com](https://kozieiev.com/blog/packaging-clojure-into-jar-uberjar-with-tools-build/).

## Table of contents
- [Video version of this article](#video-version-of-this-article)
- [Jars and uberjars](#jars-and-uberjars)
  * [jar/uberjar content](#jar-uberjar-content)
    + [Resouces](#resouces)
    + [`MANIFEST.mf`](#-manifestmf-)
    + [`pom.xml`](#-pomxml-)
- [`tools.build` library](#-toolsbuild--library)
  * [adding `tools.build` to a project](#adding--toolsbuild--to-a-project)
  * [creating a jar for the library](#creating-a-jar-for-the-library)
  * [creating a runnable ubjerjar](#creating-a-runnable-ubjerjar)
  * [more `tools.build` functions](#more--toolsbuild--functions)

## Video version of this article
{% embed https://www.youtube.com/watch?v=OgV-ALpmXUI %}


## Jars and uberjars

The most common way to prepare your Clojure project for distribution is to pack it into a `*.jar` file.

JAR format came from the Java world and it stands for "Java ARchive". It is a **zip** archive with a `*.jar` extension that contains Java class files, resources, and metadata.

To distribute Clojure library you can create a jar with library's source files or with compiled code (Java *.class files with bytecode). Optionally you can put both - source files and compiled files. Compiled files will be preferred over a source files unless source files are newer.

To distribute a Clojure application you have to create a jar that contains compiled Clojure code of the app along with all its dependencies. Such self-contained archive called **uberjar**.



### jar/uberjar content

In addition to Clojure sources and Java bytecode files, jars can contain resources and metadata files.

#### Resouces

When preparing the jar you can put there any resource files that your library or app needs (images, text, etc). From the Clojure code, those resources can be accessed via [`(io/resource)`](https://clojuredocs.org/clojure.java.io/resource) function.

#### `MANIFEST.mf` 

Manifest is a special file that contains information about jar content. Here can be specified the entry point of an application but you won't need to do that manually.  `tools.build` will generate a default manifest for every jar automatically. If you will need to extend manifest with custom fields, you can use `:manifest` option in `(jar) ` and `(uber)` functions of `tools.build`.

More info about manifest can be found in [Java documentation](https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html).

#### `pom.xml`

In the Java world, there is a super-popular buiid framework called `Maven`. it uses `pom.xml` files to store project information and configuration. This framework also has its own type of repository for built projects called "maven repository". 

In Clojure, we don't have to use Maven to build projects but we extensively use maven repositories to store build artifacts (jars). Dependencies of our project could be downloaded from maven repos and the resulting artifact of our project (its jar) can be uploaded to maven repo to be accessible by others. 

The most popular maven repo for Clojure artifacts is [Clojars](https://clojars.org/).

If you are planning to upload your project's jar to some maven repository, that jar should contain a `pom.xml` with artifact info.

More info about POM can be found in [maven documentation](https://maven.apache.org/guides/introduction/introduction-to-the-pom.html).



## `tools.build` library

Not so long ago you had to use 3rd-party tools to create jars for your own Clojure projects. But now the official [`tools.build`](https://github.com/clojure/tools.build) library can be used.

The main idea behind `tools.build` is that project's build is also a program and it can be written in Clojure code. And `tools.build` is a library that provides functions commonly needed for builds. 



### adding `tools.build` to a project

Let's imagine that we have a simple project structured like this:

```bash
project
├── deps.edn
└── src
    └── ...
```

To start using `tools.build` we need to:

- create a Clojure namespace to keep build functions
- add a new alias to `deps.edn` to call build functions with its help
- invoke build function via Clojure CLI

Let's add a `build.clj` to the root of the project. This is where we are going to keep all the build tasks. For now, there will be only one function - `(clean)` . It removes `target` directory using `(b/delete)`  from [`tools.build.api`](https://clojure.github.io/tools.build/clojure.tools.build.api.html). Inside `target`, we will keep all our build artifacts, so it should be cleaned between builds.

We don't use any arguments inside `(clean)`  but it should be defined as a function with one argument because, when invoked from Clojure CLI, it will be called with one - map of the arguments passed via the command line (`nil` if you didn't pass any).

`build.clj`:

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

(def build-folder "target")

(defn clean [_]
  (b/delete {:path build-folder})                                 ; removing artifacts folder with (b/delete)
  (println (format "Build folder \"%s\" removed" build-folder)))
```



To make build functions accessible via Clojure CLI, let's create a new alias in `deps.edn`. In that alias there should be `:deps` key with `tools.build` dependency and `:ns-default` key which tells CLI in what namespace it should look for the function mentioned in the command line. 

`deps.edn`:

```clojure
{
; ... other deps.edn content ...
:aliases
  {
    ; ... other aliases ...
  
    ; build alias:
    :build {:deps {io.github.clojure/tools.build {:git/tag "v0.8.1" :git/sha "7d40500"}}
            :ns-default build}}  ; <-- set build namespace as default
}
```



And finally, to run `(clean)` from the command line using the new alias, we need to call Clojure CLI with `-T` option. That option ignores the rest of `deps.edn` content and uses the dependencies only from the current alias. In this way, we do not interfere with the other project dependencies and source files.

```bash
$ clj -T:build clean
Build folder "target" removed
```



### creating a jar for the library

Now let's review an example of creating a jar for the library with the following folder structure
```
mymathlibary
├── build.clj
├── deps.edn
├── resources
│   └── lib_resource.txt
└── src
    └── mymath
        └── sum.clj
```

Inside `sum.clj` there is a `sum` function that adds two values and prints text from the resource file (please promise me to not do like this in real-world libs). 

`sum.clj`:

```clojure
(ns mymath.sum
  (:require [clojure.java.io :as io]))

(defn sum [a b]
  (println "Lib resource content:" (slurp (io/resource "mymath_resource.txt")))
  (+ a b))
```

Text inside `lib_resource.txt`:

```
hello from mymath lib
```



Let's say we want our jar to contain only source code, without compiled bytecode. To achieve this, in `build.clj` we need to add a new function named `(jar)` that does the following:

- cleans `target` directory from leftovers
- copies sources and resources to `target`. They should go into the result jar.
- creates `pom.xml` file. You will need it if you are going to put the library into maven repositories.
- creates the jar file

Here is the code of our new `build.clj`:

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

(def build-folder "target")
(def jar-content (str build-folder "/classes"))     ; folder where we collect files to pack in a jar

(def lib-name 'com.github.YOURNAME/mymath-lib)      ; library name
(def version "0.0.1")                               ; library version
(def basis (b/create-basis {:project "deps.edn"}))  ; basis structure (read details in the article)
(def jar-file-name (format "%s/%s-%s.jar" build-folder (name lib-name) version))  ; path for result jar file

(defn clean [_]
  (b/delete {:path build-folder})
  (println (format "Build folder \"%s\" removed" build-folder)))

(defn jar [_]
  (clean nil)                                     ; clean leftovers

  (b/copy-dir {:src-dirs   ["src" "resources"]    ; prepare jar content
               :target-dir jar-content})

  (b/write-pom {:class-dir jar-content            ; create pom.xml
                :lib       lib-name
                :version   version
                :basis     basis
                :src-dirs  ["src"]})

  (b/jar {:class-dir jar-content                  ; create jar
          :jar-file  jar-file-name})
  (println (format "Jar file created: \"%s\"" jar-file-name)))
```



In the code of `(jar)` we use three functions from `tools.build` to achieve steps mentioned before: `(b/copy-dir)`, `(b/write-pom)` and `(b/jar)`. Their names and arguments are pretty self-explanatory except one - `:basis`.

The basis is a big structure that contains a superset of all `deps.edn` files, project classpath, and description of all dependencies. In the official Clojure documentation, it is mentioned [here](https://clojure.org/reference/deps_and_cli#_basis_and_classpath). 

`tools.build` uses basis in a few functions and gives a function to create it - `(b/create-basis)`. The `(b/write-pom)` function uses basis to correctly reflect the library dependencies in `pom.xml`

Now to create the jar we can run `(jar)` function using Clojure CLI (assuming your `deps.edn` already contains `:build` alias):

```bash
$ clj -T:build jar
Build folder "target" removed
Jar file created: "target/mymath-lib-0.0.1.jar"
```

Now it can be uploaded to a maven repo and used as a dependency in other projects.

You can examine jar content using `jar -tf` command:

```bash
$ jar -tf target/mymath-lib-0.0.1.jar 
META-INF/MANIFEST.MF
lib_resource.txt
META-INF/
mymath/
META-INF/maven/
mymath/sum.clj
META-INF/maven/com.github.YOURNAME/
META-INF/maven/com.github.YOURNAME/mymath-lib/
META-INF/maven/com.github.YOURNAME/mymath-lib/pom.xml
META-INF/maven/com.github.YOURNAME/mymath-lib/pom.properties
```

[![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7v0zsd3bxcdkmmeogot3.png)](https://www.buymeacoffee.com/volodymyrk)

### creating a runnable ubjerjar

Now we can take a look at how to create a runnable uberjar for a Clojure application.

For example, we have a project with the following structure:

```
simpleapp
├── build.clj
├── deps.edn
├── resources
│   └── app_resource.txt
└── src
    └── dev
        └── core.clj
```

`core.clj ` contains `(-main)` function that will work as an entry point when the application invoked. The `(-main)` calls `(sum)` function from the library created in a previous section and prints text from the local resource file.

`core.clj`:

```clojure
(ns dev.core
  (:require [mymath.sum :as s]
            [clojure.java.io :as io])
  (:gen-class))     ; !!! instruction to generate bytecode for java class from this namespace

(defn -main []
  (println "Sum is:" (s/sum 1 2))
  (println "App resource content:" (slurp (io/resource "app_resource.txt"))))
```

Most important thing here is a `:gen-class` directive to `(ns)` function. It tells Clojure compiler to generate an additional bytecode file with java class corresponding to this namespace. That class will serve an entry point for our uberjar. 

Now in `build.clj` we can create an `(uber)` function that builds uberjar. Here is what it should do:

- clean `target` directory from leftovers
- copy resources to `target`. They should go into result uber.
- compile Clojure code
- create the uberjar with `(-main)` entrypoint 

Full code of `build.clj`:

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

(def build-folder "target")
(def jar-content (str build-folder "/classes"))

(def basis (b/create-basis {:project "deps.edn"}))
(def version "0.0.1")
(def app-name "myapp")
(def uber-file-name (format "%s/%s-%s-standalone.jar" build-folder app-name version)) ; path for result uber file

(defn clean [_]
  (b/delete {:path "target"})
  (println (format "Build folder \"%s\" removed" build-folder)))

(defn uber [_]
  (clean nil)

  (b/copy-dir {:src-dirs   ["resources"]         ; copy resources
               :target-dir jar-content})

  (b/compile-clj {:basis     basis               ; compile clojure code
                  :src-dirs  ["src"]
                  :class-dir jar-content})

  (b/uber {:class-dir jar-content                ; create uber file
           :uber-file uber-file-name
           :basis     basis
           :main      'dev.core})                ; here we specify the entry point for uberjar
  
  (println (format "Uber file created: \"%s\"" uber-file-name)))
```

The code is quite similar to the one where we were creating jar and probably doesn't require many explanations. New functions here are `(b/compile-clj)` and `(b/uber)`. 

By the way you could have used `(b/compile-clj)` in library example to create a library jar with compiled code. 


Now let's bulid our uberjar:

```bash
$ clj -T:build uber
Build folder "target" removed
Uber file created: "target/myapp-0.0.1-standalone.jar"
```

And run it using `java -jar` command:

```bash
$ java -jar target/myapp-0.0.1-standalone.jar
Lib resource content: hello from mymath lib
Sum is: 3
App resource content: hello from app
```

As we can see, resource file content was printed from the app and the lib. That means uberjar was created successfully and contains all the necessary files.

The output of `jar -tf target/myapp-0.0.1-standalone.jar` will be too long to post it here but I encourage you to investigate the content of created uberjar to see how dependencies are included there. 



### more `tools.build` functions

Your `build.clj` is not restricted to creating jars and uberjars. There are more functions in `tools.build` that give you freedom to build scripts of an arbitrary complexity:

`(process)` - runs an arbitrary command with arguments

`(git-process)` - runs git command

`(zip)` and `(unzip)` - work with archives

`(install)` - installs jar to a local maven repository

More details can be found in the `tools.build` [official api](https://clojure.github.io/tools.build/clojure.tools.build.api.html#var-install).