Feeling The Code
I don’t agree with the opinion that “cool kids now use boot“. People who say that are just missing out on the power of “feeling the code” rather than being abstracted from the code by a “better XML”. Same deal with people 10 years ago who said “cool kids are using functional languages”.
Don’t get me wrong I like lein a lot. It is simple to start with, it is well documented, it is very googlable, it is sharing platform (i.e. templates), mature, etc.. But boot is very different, it does not aim to do what lein does, it aims to do “what you want”. There is a difference.
Mounting a Bootable Partition
Since the late 90s when I got in to Linux, I found bootable partitions most exciting, they actually bootstrap everything, they were these wizards waving their magic wands and systems appeared. Granted the wave could take minutes, but we are humans, we always wait for the magic, even if it takes the whole life.
First thing that needs to be done for the magic to happen, this bootable partition needs to be mounted.
I wanted to do it for some time now, when, I could not figure out why ClojureScript brought in as a dependency with :classifier “aot” caused compilation problems with lein/cljsbuild, David Nolen suggested that this is rather due to the lein environment issues. So 2 and 2 together: it was the right time to “boot” myself up.
And since the partition was already mounted it was ready to boot.
Grokking the New Simple
Rather than tell you how great boot is, I’ll share non obvious (to me) things that I stumbled upon converting mount from lein to boot. Let’s rock & roll:
REPL is just REPL
Since I needed a support for both Clojure and ClojureScript, I looked at many examples and noticed a pattern: usually in a dev mode one task groups several, where most of the examples have a (watch) task in that group.
I just wanted to start out, so I decided that at a minimum I need a REPL and (I guess) this watcher to be able to mimic the lein repl behavior, so I did:
(deftask dev [] (comp (watch) (repl))) |
And it worked! I ran boot dev and I got a REPL which would see all the updates from vim (via the updated vim-fireplace).
But then I decided to stop the REPL, and it just froze.. I ran jstack on the PID and saw lots of watcher threads locking and derefing futures. Ok, so that’s not a good combination.
The answer is simpler than I expected: it’s just boot repl. Nothing else is needed to get to the lein repl functionality.
“Bring on Your Own Data Readers” Party
The Clojure mount example app uses in memory Datomic, so when I tried to start the app, boot told me:
no reader to handle the #db/id tag |
This was easily googlable, and revealed that boot has a (load-data-readers!) function that “refreshes *data-readers* with readers from newly acquired dependencies”.
An interesting bit here is that (load-data-readers!) can’t be a part of a “top level” task that is executed with boot since:
java.lang.IllegalStateException: Can't set!: *data-readers* from non-binding thread |
So calling boot dev, in case “load-data-readers!” is there, is not an option. But getting into a REPL “boot repl“, and then calling (dev) works beautifully.
REPL Logging
At this point I could get into the boot REPL and start the mount example app. A slight problem was that I did not see any logging from the app within the REPL.
That’s when I found boot-logservice that brought the logging back to the REPL:
(def log4b [:configuration [:appender {:name "STDOUT" :class "ch.qos.logback.core.ConsoleAppender"} [:encoder [:pattern "%-5level %logger{36} - %msg%n"]]] [:root {:level "TRACE"} [:appender-ref {:ref "STDOUT"}]]]) ;; ... (deftask dev [] ;; ... (alter-var-root #'log/*logger-factory* (constantly (log-service/make-factory log4b))) ;; ... ) |
Shaking up tools.namespace
While it is not a requirement, and most of the time unnecessary, the example app uses tools.namespace to make it easier for people who rely on it heavily to get into mount.
By default “tools.namespace” won’t find anything to refresh, since boot uses its own “secret” temp directories for sources, and “tools.namespace” simply does not know about them.
This was an easy one, since it is well documented by boot. Hence having (apply set-refresh-dirs (get-env :directories)) in the “dev” task pointed “tools.namespace” to the right directories to refresh.
The Joy of Deploy: Build and Publish
At this point having the Clojure part figured out, before moving to the ClojureScript support, I decided to deploy mount to Clojars, to understand how it’s done with boot.
I found bootlaces, and just plugged it in, it was very straightforward:
(def +version+ "0.1.7-SNAPSHOT") (bootlaces! +version+) ;; other things.. and (task-options! pom {:project 'mount :version +version+ :description "managing Clojure and ClojureScript app state since (reset)" :url "https://github.com/tolitius/mount" :scm {:url "https://github.com/tolitius/mount"} :license {"Eclipse Public License" "http://www.eclipse.org/legal/epl-v10.html"}}) |
Then I did:
boot build-jar push-snapshot |
and everything was going smoothly, it asked for my Clojars username, then password.. but then:
clojure.lang.ExceptionInfo: java.lang.AssertionError: Assert failed: current git branch is 0.1.7 but must be master (or (not ensure-branch) (= b ensure-branch)) |
Boot told me that it prefers publishing snapshots from the “master”. I don’t disagree, but for some projects I like snapshots from version branches. I don’t really like “git flow”, I like “git freedom”.
Looking at the bootlaces code it seems that “master” is hardcoded. By this time I already started to feel the concept of a “boot task” and noticed that it is hardcoded under the “push” internal task, which means that this task’s options can potentially be overridden:
;; ... (task-options! push {:ensure-branch nil} ;; <<<<<<<<<< pom {:project 'mount :version +version+ ;; ... }) |
And what d’you know, it worked! This was most likely the first “aha moment” which wired some of my neurons in boot ways.
Shall Not Pass!
Mount’s “test” root has both cljc tests and clj/cljs test apps that these tests use. The structure looks similar to:
|~test/ | |~clj/... | | `+tapp/ | |~cljs/... | | `+tapp/ | |~mount/ | | |+test/... | | `-test.cljc |
In lein, I can give “test” + “test/clj” for Clojure tests, and “test” + “test/cljs” for ClojureScript tests as the sources paths.
In boot I can’t do that, boot says:
java.lang.AssertionError: Assert failed: The :source-paths, :resource-paths, and :asset-paths must not overlap. (empty? (set/intersection paths parents)) |
Since boot already read everything under “test”, it does not want to merge things from “test/clj”. Fair enough, so I had to change the structure a bit to make it work:
|~test/ | |~clj/ | | `+tapp/ | |~cljs/ | | `+tapp/ | |~core/ | | `~mount/ | | |+test/ | | `-test.cljc |
Now I can give “test/core” + “test/clj” and “test/core” + “test/cljs” respectively.
ClojureScript is Clojure, but.. not Always
ClojureScript took some time to get right. Many examples helped a lot especially these three: boot-cljs-example, tenzing and boot-cljs-multiple-builds.
The concept of dividing “cljs” options between “xyz.cljs.edn” and “task options” did not sink in immediately, and required some code digging to figure out where to put what and how to make sure it is being used.
It ends up to be quite simple. Options that are provided via “xyz.cljs.edn” can be referenced from task options via ids option:
(cljs :optimizations :advanced :ids #{"mount"}) |
would mean that it would look for mount.cljs.edn file within the classpath. That file should point to the entry point of the ClojureScript app. In case of the mount example app it would just be:
{:require [app.example]} |
where init-fns and compiler-options can be also added.
Testing ClojureScript
“mount does doo” for ClojureScript testing, and boot-cljs-test does it as well.
I would expect it to pick up “xyz.cljs.edn” files in the same way as “boot-cljs”, but it does it a bit differently. It is not all that obvious at first, but looking at the code I saw that it has a different name for ids, it calls it out-id. It also does not just take an “id”, it takes an “id” + “.js”, as I saw from the code.
So to get it to work is quite simple:
(tcs/test-cljs :out-file "mount.js")) |
which would look for the same mount.cljs.edn file within the classpath.
Power it Up
There were other discoveries, like
* tasks are functions, but not really, they take arguments in the particular format and they better return a fileset
* tasks: “comp us please”. They like to be (comp ..)ed. Otherwise no go.
* there were others, but I liked Pods the most.
At this point I got all up and pumping, deployed to CircleCi using boot to build and run tests, published to Clojars as snapshot and release, etc.
One of the greatest things that I loved while debugging dependencies is boot show -p, it’s amazing!
Get up! Boot yourself up! Enjoy the runtime!