Giving clojurescript the boot#
Mon, 19 Sep 2016 21:58:57 +0000
Recently I decided to unearth Sledge and fix enough of the bugs in it that I will actually want to use it day-to-day, and because changing only one thing at a time is too easy I thought I'd try updating all its dependencies and moving it from Leinginen to the new shiny Boot Clojure build tooling.
First impressions of Boot:
- it appears to be a much more general purpose build tool than Leiningen in that it prefers you to compose your build process from self-contained parts instead of being a battalion of special-cases.
- the documentation of how to do ordinary things (like "build a jar file that you can give to someone who doesn't have Clojure already") is somewhat lacking
- Googling won't save you either: accounts of its use on third-party sites are often written for older versions
Point 1 is in my judgment so far a compelling reason to perservere through the pain of points 2 and 3.
I did a little gist already for making an uberjar with Boot and you should definitely pay attention to line 25. But today I want to talk about defining your own tasks, and as an example I'm going to add support for building Clojurescript.
The well-informed reader will know that there is already a Boot task to compile ClojureScript programs on Github. I chose not to use this, mostly because I was getting error messages I didn't understand and also partly because the Clojurescript Quick Start wiki page so strongly recommends understanding the fundamentals of compiling Clojurescript before plugging in a tool-based workflow.
So. Here are some things you may or may not already know about Boot if you've previously been using it by cargo-cult:
- You tell Boot what you want it to do by composing tasks into a build pipeline
- Each task defines a handler . A handler accepts a fileset, does something (e.g. compiles some files to create output files) and then calls the next handler with a new fileset which represents the passed fileset plus the result of whatever it did. It's a lot like Ring nested middleware. The first handler in the pipeline gets a fileset consisting only of the input files (your project source files), calls the second which calls the third etc, and once all the nested handlers have run then the returned fileset contains all the output files.
- From the end-user or even the task author's point of view, filesets are immutable. Handlers never directly change or create files in the project directory - instead they always create a new fileset, and Boot copies/moves files around in temporary directories behind your back to maintain the abstraction. As a task writer you don't have to care too much how this works: there are functions to map between fileset entries and the full pathname you should use to read the corresponding file; also to create a new temporary directory for output and then to add the files in that directory to a fileset.
And here are some things about the Clojurescript compiler which probably should be apparent from reading the Quick Start and the code it refers to:
- The compiler API lives in ns cljs.build.api and the important bits are inputs which creates a 'compilable object' from the directories/files you give it, and compile which does the compilation.
- Contrary to anything you might have thought by reading the doc
string of
inputs
- it does not accept "a list", it accepts multiple arguments. If you have a list you will need to useapply
here. I wasted a lot of time on this by not reading the code properly.
So, how do we marry the two up? Look upon my task ye mighty and despair ....
A task is a function that returns a middleware, which is a function
that returns a handler. This is not super-obvious from the code on
display here, because we're using a small piece of handy syntax sugar
called with-pre-wrap
which lets us provide the body of the handler
and returns a suitable middleware.
What else? Not much else. This code lives in the sledge.boot-build
namespace and gets require
d by
build.boot
. We have to override and/or augment what the user passes for
output-to
and output-dir
to make sure it ends up somewhere it'l
get added to the fileset instead of writing straight into the project
working directory. And I haven't decided how to do a repl yet. I
will probably add that (one way or another) before merging the
das-boot
branch into master
.