Boot has the concept of immutable filesets. Each task receives a
fileset and produces one. The last task outputs its fileset to a
target directory, which is target by default. Boot will clean stale
files from target automatically before it emits a new fileset
there. There is never a need to clean something in Boot.

Dependencies

Next we describe which dependencies the project has. In Leiningen this
is done as follows:

Initial REPL namespace

This makes the animals.server namespace the starting point for every
REPL session. In build.boot it is accomplished like this:

1

(task-options!repl{:init-ns'animals.server})

Boot has several tasks built in and repl is one of them. It supports
the option init-ns. With task-options! you can set some default
options per task that are global to the project. To see all the
options that repl provides, you can issue -h from the command
line:

$ boot repl -h

or call (doc repl) from a Boot REPL session.

Global var setting

Next is this line from project.clj:

1

:global-vars{*print-length*20}

This sets the var clojure.core/*print-length* to 20. If we print collections we’ll never see more than 20 items:

123

user=>(println (range))(012345678910111213141516171819...)nil

In Boot I attempted to do it like this:

1

(alter-var-root(var *print-length*)(fn [v]20))

Unfortunately this caused a
bug in Boot’s jar
task. Later I learned that it’s not a good idea at all to do this in Boot, because there can be multiple Clojure runtimes (pods) in one JVM. Since I was going to use this setting only in the REPL, this is a better solution:

Task dependencies

Leiningen has the concept of
plugins. Plugins
typically perform a task as part of a Leiningen build. Two popular
plugins are
lein cljsbuild and
lein figwheel. lein
cljsbuild is an interface to the ClojureScript compiler. lein
Figwheel lets you push resources to a browser, typically freshly
compiled ClojureScript or CSS. It also gives you a ClojureScript REPL
and a web server which allows you to serve some static files or even a
Ring handler. In this example I don’t use Figwheel’s web server for
running my Ring handler, because I use Ring’s standalone Jetty server
that comes with automatic code reloading middleware and allows for an
initial function to be executed before the handler is started:
features that are lacking from Figwheel as far as I know.

In Leiningen plugins are included like this:

12

:plugins[[lein-cljsbuild"1.0.5"][lein-figwheel"0.3.1"]]

In Boot tasks are included as normal dependencies and scoped with :test:

The first dependency is Boot’s interface to the ClojureScript
compiler. The latter three dependencies together offer more or less
the same as Figwheel: a ClojureScript, live reloading of resources in the browser
and a web server to serve static files or a Ring handler.

Building ClojureScript

In Leiningen ClojureScript is built using the lein cljsbuild plugin. My
configuration for this plugin looks as follows:

In Boot configuring the location of where generated JavaScript will
end up is done by placing an .edn file at the corresponding location
in the resources tree. In this project I placed it in
resources/public and named it main.cljs.edn with the following
content:

12

{:require[animals.main]:compiler-options{:asset-path"out"}}

This gives you the same config as in the Leiningen example with
respect to the name of the main JavaScript file, :asset-path
and the :main namespace.

I use different source folders for development and production, so I
can have some environment specific configuration. For example, in
development I enable console print and define a function for Figwheel
that will be executed when new ClojureScript is pushed to the browser:

There is no need to worry about cleaning directies, since each task
outputs an immutable fileset that the next task can use. Generated
files end up in target by default, which gets cleaned before a new
fileset is committed there.

The serve task will be the first one to be invoked. By default serve runs a Jetty server, but it is possible to select http-kit. It will reload Clojure files automatically, is able to run a Ring handler and also executes an initial function before the handler is started.

The next task in our Boot pipeline is watch. This task waits for
file changes in any of the source or resource paths and then invokes
the rest of the pipeline. The rest of the pipeline is also invoked one
time for the initial fileset. An example:

1234

(deftaskwatch-example[](comp(watch)(show:filesettrue)))

In this example show prints out the fileset that it received as a
tree. When we invoke it we see the initial fileset tree. When we add a
file, the watch task will invoke show again and we would see the new
fileset tree with the added file.

Whenever a file changes, the reload task is invoked. This will send
changed assets to the browser via a websocket connection. The task
after that, cljs-repl starts an nrepl server in which it is possible
to start a ClojureScript REPL. This task also covers our requirement
to have a normal Clojure REPL session with our program. Finally, the
cljs task compile ClojureScript to JavaScript. I am not sure if it
matters if cljs-repl comes before or after watch, but cljs
surely has to come after it, since it has to see new filesets for
incremental compilation.

Standalone jar

The final requirement is producing a standalone jar. In Leiningen this
is done with the uberjar task. We need to tell Leiningen where it
can find the main namespace that will have the -main function that
will be invoked when the jar is run. Also we need to aot that namespace:

12

:aot[animals.uberjar]:mainanimals.uberjar

Before producing a standalone jar, we must invoke the ClojureScript
compiler to produce production worthy JavaScript. For convenience we
can make an alias that combines all these steps:

1

:aliases{"build"["do""clean"["cljsbuild""once""prod"]"uberjar"]}

Note that uberjar will invoke clean also. One problem that arose
while writing this blog was that I had the entire out directory in
:clean-targets, so when cljsbuild was done, uberjar would remove
its output again. You will never have this kind of problem with Boot.

First we invoke the sub-task build-cljs which includes sources from
src-cljs-prod and produces optimized JavaScript. The next task performs aot on the main namespace. Then a pom file is produced. The uber task adds jar entries from dependencies to the fileset. Finally, the jar task produces a jar file from the fileset with the main namespace set to animals.uberjar. I love how Boot decomposes these tasks, so you can actually see what is going on when you produce a standalone artifact.

Issues

During this blog post I ran into a couple of issues with Boot.

The first issue had to do with dependency resolution and Clojure versions. This issue has been solved. Thanks Alan Dipert!

Another issue was that the reload task didn’t have the concept of an asset-path.
I needed to work around this by creating an extra route in my handler:

This problem will be solved in a future version of reload. See this
issue.

Conclusion

Leiningen is a battle tested tool and probably the safest bet if
you’re just starting with Clojure. However, Boot has certainly sparked
my interest. It has an elegant design and a more functional feel to
it. I’ll certainly use it on a future project.

Here are my recommendations based on my brief experience with Boot.

Use Leiningen if you:

want to get started fast and like to get help from the majority of
the Clojure community

don’t want to take risks in terms of stability

like configuration and convenience over programmability and composition

Use Boot if you:

like programs more than configuration files

don’t like to think about cleaning directories

need to run one JVM for the entire development process (in
Leiningen I needed two: one for Clojure and one for ClojureScript)