Useful Stuff in Clojure - Deployable Artifact

2018-06-07

Let's do some useful stuff in Clojure. A lot of tutorials focus on things like why lazy sequences are great or what you can do with transducers.

These are both cool things, but Clojure is, I think, at a singular sweet-spot of utility and clarity.

Let's get started.

The quickest useful thing we can do is share our work with someone. Regardless of what our work is, it's useful to be able to share it, and Clojure makes this pretty easy to do.

For this series we're going to use Boot, which is a comparatively new-ish build tool in the Clojure ecosystem. Leiningen is the other build tool in Clojure, and most of what I talk about will apply to both.

First, you're going to need to install Boot. If you're on Mac, you can brew install boot-clj. Linux should be similar.

Then, we're going to create a new Clojure project with Boot:

$ boot -d boot/new new -t app -n deployable

This says "make a new project called deployable, with the app template."

Templates are a feature of Boot and Leiningen that allow you to quickly create new Clojure projects in a way that is similar to how you use something like Rails generators to quickly create a new controller or model. You can create your own templates, and we might explore that in a future tutorial, but for now, you'll have a new directory in your working directory called deployable containing your project.

It will look like this:

clark$> tree
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.boot
├── doc
│   └── intro.md
├── resources
├── src
│   └── deployable
│       └── core.clj
└── test
    └── deployable
        └── core_test.clj

The only two we really care about at this point are build.boot and core.clj. build.boot is the file that describes your project and how to build it, similar to a Makefile or a combination Gemfile/Rakefile.

I'll explain it more in a sec, but first let's build and run the project:

$ boot build

If you ls the target directory, you should see a few things, among them a file named deployable-0.1.0-SNAPSHOT-standalone.jar.

This is the artifact of your project that you can share with other people or run on a server somewhere. The only caveat is that they need to have a JVM compatible with the version of the JVM with which you compiled the project.

Run the project like:

$ java -jar target/deployable-0.1.0-SNAPSHOT-standalone.jar

If you see "Hello, World!", you did everything right!

Back to the details of what we just did.

core.clj is the entrypoint of your project. If you open it up, and you'll see the main function of your project.

(ns deployable.core
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

This function, -main, which takes a variable number of args, ([& args]), is what will receive any args you pass to your program when you invoke it on the command line.

The -main function generated by Boot prints "Hello, World!"`, but we could make ours do anything.

If we wanted to make it print the first argument, we could add this snippet:

(when (first args)
  (println (first args)))

Now we can do:

clark$> java -jar target/deployable-0.1.0-SNAPSHOT-standalone.jar hi
Hello, World!
hi

Back to the build.boot file, you'll see a few things. Only a few are relevant right now. Those are deftask build and the section of task-options! next to jar.

The build task is what will take our program and emit what in Clojure is known as an "uberjar". This file, with the .jar extension, is the executable format of the JVM landscape. The "uber" in the name refers to the fact that it will include all of our project's dependencies (if it has any), rather than relying on them being somewhere else on our machine's filesystem, like you would in Ruby or Python.

We don't have to get super into detail, but the build task composes a few other sub-tasks to ultimately produce our jarfile into our project's target directory, while the jar subsection of task-options! tells the build process what our project's entrypoint is, and what the jarfile should be called.

In our case, you see that our jar file will have the name of the result of this expression: (str "deployable-" version "-standalone.jar"), and the entrypoint will be deployable.core. In Java or other OOP terms, you can think of deployable.core as the "main class".

To drive this post a little further into useful territory, let's learn how to add dependencies to our project. This will clarify the "uber" bit of uberjar, as the build step will remain the same, and the target, whether it's our friend's computer or a server, will not have to adjust to the presence of a dependency at all.

Take a look at the section of the build.boot that looks like this:

(set-env! :resource-paths #{"resources" "src"}
          :source-paths   #{"test"}
          :dependencies   '[[org.clojure/clojure "RELEASE"]
                            [adzerk/boot-test "RELEASE" :scope "test"]])

Notice how it is a series of key-value pairs? THe keys are the keywords :resource-paths, :source-paths, and :dependencies. The values are the terms that follow each respective key. Right now, we care about the one following :dependencies.

We're going to make our program do something with JSON, so we're going to add the Cheshire dependency.

To do that, change the :dependencies value from:

'[[org.clojure/clojure "RELEASE"]
  [adzerk/boot-test "RELEASE" :scope "test"]]

to:

'[[org.clojure/clojure "RELEASE"]
  [cheshire "5.8.0"]
  [adzerk/boot-test "RELEASE" :scope "test"]]

Notice how we added [cheshire "5.8.0"].

Now you can turn back to core.clj, where we're going to pull in the functionality from Cheshire that we need, and hook it into our program:

(ns deployable.core
  (:require [cheshire.core :as json])
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!")
  (when (first args)
    (println (json/decode (first args)))))

After rebuilding ($ boot build), we see the result of our changes:

clark$> java -jar target/deployable-0.1.0-SNAPSHOT-standalone.jar '{"hi":"folks"}'
Hello, World!
{hi folks}

That's it! Now you can tell all of your Golang friends that your language builds static binaries too!