Clojure + deps.edn, a basic guide.
After a rather long break from programming and Clojure I decided give them another go. When it comes to managing Clojure projects, Leiningen is de-facto standard tool. Recently, Clojure CLI tools are becoming more and more popular, though. Switching to yet-another-build-tool doesn't have any pragmatic value, but it's perfect for learning purposes.
From a build tool I expect it to perform certain tasks:
- Creating a project.
- Managing source and tests paths.
- Managing dependencies.
- Running tests.
- Building a self-contained JAR, a.k.a. uberjar.
- Managing outdated dependencies.
Let's see how it's performed using Clojure CLI tools, a.k.a. deps.edn.
Leiningen allows to generate a project structure simply by invoking:
$ lein new [template] [project-name]
We get a lot for free, but is it really needed? How is it done with Clojure CLI tools?
Imagine a simple project. It allows add and divide numbers, it also prints some example calculations when invoked. We can start by simply creating a new directory:
$ mkdir cdeps && cd cdeps
Now, let's add an empty deps.edn
file:
;; /deps.edn
{}
And now we can start adding some actual code to the project.
To demonstrate the feature of managing source paths we will put our code at src/main/clojure
.
$ mkdir -p src/main/clojure
deps.edn
is no magic so we can just set the path in the file:
;; deps.edn
{:paths ["src/main/clojure"]}
Now, we can write the calculator code:
;; src/main/clojure/com/tomekw/cdeps/calculator.clj
(ns com.tomekw.cdeps.calculator)
(defn plus [a b]
(+ a b))
(defn divide [a b]
(/ a b))
In such a simple project there is no real need to add external dependencies. We can always specify the Clojure version we would like to use, though:
;; deps.edn
{:paths ["src/main/clojure"]
:deps {org.clojure/clojure {:mvn/version "1.10.1"}}}
Clojure CLI tools allow to specify local and git dependencies too, see documentation and more examples.
The calculator we wrote is super simple but we can still write some tests:
;; test/main/clojure/com/tomekw/cdeps/calculator_test.clj
(ns com.tomekw.cdeps.calculator-test
(:require [clojure.test :refer :all]
[com.tomekw.cdeps.calculator :refer :all]))
(deftest adding-numbers
(is (= 4 (plus 2 2))))
(deftest dividing-numbers
(is (= 2 (divide 4 2))))
(deftest dividing-numbers-by-zero
(is (thrown? ArithmeticException (divide 1 0))))
Now we need to run them to make sure they pass. We have to add an alias (a command we will run), and a test runner, as an extra dependency. I picked kaocha. Also, we need to tell the runner where the tests are located:
;; deps.edn
{:paths ["src/main/clojure"]
:deps {org.clojure/clojure {:mvn/version "1.10.1"}}
:aliases {:test {:extra-paths ["test/main/clojure"]
:extra-deps {lambdaisland/kaocha {:mvn/version "0.0-529"}}
:main-opts ["-m" "kaocha.runner"]}}}
Here is the test report:
$ clj -Atest
[(...)]
3 tests, 3 assertions, 0 failures.
Presume, we would like to print example calculations to the console. Let's add the code to do that:
;; src/main/clojure/com/tomekw/cdeps/core.clj
(ns com.tomekw.cdeps.core
(:gen-class)
(:require [com.tomekw.cdeps.calculator :refer :all]))
(defn -main [& args]
(do (println (format "2 + 2 is %s" (plus 2 2)))
(println (format "4 / 2 is %s" (divide 4 2)))))
To run the main function we can invoke the following command:
$ clj -m com.tomekw.cdeps.core
2 + 2 is 4
4 / 2 is 2
It could be burdensome for the users of our calculator to install Clojure. To avoid this, we can package our project as a standalone Java JAR. There is number of tools to do that, like cambada, but I've decided to try out uberdeps. Let's add a proper configuration first:
;; deps.edn
{:paths ["src/main/clojure"]
:deps {org.clojure/clojure {:mvn/version "1.10.1"}}
:aliases {:test {:extra-paths ["test/main/clojure"]
:extra-deps {lambdaisland/kaocha {:mvn/version "0.0-529"}}
:main-opts ["-m" "kaocha.runner"]}
:uberjar {:extra-deps {uberdeps {:mvn/version "0.1.4"}}
:main-opts ["-m" "uberdeps.uberjar" "--target" "target/cdeps-0.1.0.jar"]}}}
To package the project we simply run:
$ clj -Auberjar
[uberdeps] Packaging target/cdeps-0.1.0.jar...
+ src/main/clojure/**
+ org.clojure/clojure 1.10.1
. org.clojure/core.specs.alpha 0.2.44
. org.clojure/spec.alpha 0.2.176
[uberdeps] Packaged target/cdeps-0.1.0.jar in 567 ms
And now we can run the project with Java:
$ java -cp target/cdeps-0.1.0.jar clojure.main -m com.tomekw.cdeps.core
2 + 2 is 4
4 / 2 is 2
It's often needed to manage the versions of all dependencies we put into our deps.edn
file. There is a tool named
depot:
;; deps.edn
{:paths ["src/main/clojure"]
:deps {org.clojure/clojure {:mvn/version "1.10.1"}}
:aliases {:test {:extra-paths ["test/main/clojure"]
:extra-deps {lambdaisland/kaocha {:mvn/version "0.0-529"}}
:main-opts ["-m" "kaocha.runner"]}
:outdated {:extra-deps {olical/depot {:mvn/version "1.8.4"}}
:main-opts ["-m" "depot.outdated.main" "-a" "outdated"]}
:uberjar {:extra-deps {uberdeps {:mvn/version "0.1.4"}}
:main-opts ["-m" "uberdeps.uberjar" "--target" "target/cdeps-0.1.0.jar"]}}}
Everything should be up to date:
$ clj -Aoutdated
All up to date!
This guide covers basic use-cases in the daily workflow with Clojure. Of course there is alwats more than I presented here, like deploying the project to Clojars. The process is still not fully automated and I will try to cover it with the next post.