I'm trying to put together a workflow for building Clojure applications with Boot in CIDER. To test this workflow, I've written a small example application using Seesaw. My project structure looks like this:
example
├── build.boot
├── dev
│ └── user.clj
├── .dir-locals.el
└── src
└── example
└── core.clj
In build.boot
, I first need to configure my source paths and dependencies.
(set-env!
:source-paths #{"src" "dev"}
:dependencies '[[org.clojure/clojure "1.8.0"]
[com.stuartsierra/component "0.3.1"]
[suspendable "0.1.1"]
[seesaw "1.4.5"]
[org.clojure/tools.namespace "0.2.11"]
[reloaded.repl "0.2.1"]])
The actual source code for my application will be in src
, but I want to have a source file for my user
namespace where I can :require
other namespaces and put temporary functions during development. I don't want that file to be in source control, so I'll put it in the separate dev
directory, which I can then easily add to my .gitignore
.
The problem is, when I package my app into a JAR, I don't want to include anything from dev
in that JAR. Were I using Leiningen, I would just add {:source-paths ["dev"] :repl-options {:init-ns user}}
to the dev
profile, but Boot doesn't have profiles. The wiki recommends writing a task that calls set-env!
and returns identity
, but since tasks are middleware factories, it seems kind of weird to me to perform a side effect at the top level in a task. Wouldn't something like this make more sense?
(deftask dev []
(with-pass-thru _
(set-env! :source-paths #(conj % "dev"))))
I don't know enough about Boot filesets to discern whether this is a good idea or not. Could adding to the source paths like this cause any problems?
For this example, all the actual code is in core.clj
:
(ns example.core
(:require [com.stuartsierra.component :as component]
[seesaw.core :as seesaw]
[suspendable.core :as suspendable]))
This is a fairly contrived little application, so the details aren't terribly important, but any feedback on my approach would be helpful regardless. I want to treat a frame as a component, so I created a Frame
record implementing Lifecycle
:
(defrecord Frame [frame]
component/Lifecycle
(start [this]
(if frame
this
(assoc this :frame (seesaw/frame))))
(stop [this]
(seesaw/dispose! frame)
(assoc this :frame nil))
suspendable/Suspendable
(suspend [this]
this)
(resume [this old]
(if (:frame old)
(assoc this :frame (:frame old))
(do
(component/stop old)
(component/start this)))))
(defn frame []
(map->Frame {}))
The reason I'm using Suspendable here instead of just Component is that it's too visually jarring for me to tear down the frame and pop up a new one every time I refresh my system. Also, it seems to mess with the size of my Emacs window as well, and I'm not sure how to fix that. Reusing the existing frame allows me to sidestep that issue.
Then, to make use of the frame, I wrap it up in a system with an App
component:
(defrecord App [frame]
component/Lifecycle
(start [this]
(seesaw/config! (:frame frame) :content "Hello, world!")
(when-not (seesaw/config (:frame frame) :visible?)
(seesaw/show! (:frame frame)))
this)
(stop [this]
this))
(defn app []
(map->App {}))
(defn system []
(component/system-map
:frame (frame)
:app (component/using (app) [:frame])))
Putting CIDER aside for a minute, I can now play with a running system in the REPL. A minimal user.clj
would look like this:
(ns user
(:require [example.core :as core]
[reloaded.repl :refer :all]))
(defn sys []
(core/system))
I can use Boot's built-in REPL task and reloaded.repl
's set-init!
and go
functions to start things off:
$ boot repl -n user
user=> (set-init! sys)
user=> (go)
But I also want to be able to make changes to my source code and refresh my system automatically as I go. So I wrote a little refresh
task to do that for me:
(require '[clojure.tools.namespace.repl :as tns]
'[reloaded.repl :as repl])
(deftask refresh
[s system SYM sym "The system function."]
(with-pass-thru _
(apply tns/set-refresh-dirs (get-env :directories))
(repl/set-init!
(fn []
(require (symbol (namespace system)))
((resolve system))))
(with-bindings {#'*ns* *ns*}
(let [x ((if system repl/reset tns/refresh))]
(when (instance? Throwable x)
(throw x))))))
Now, my main question is this: How should I use this task from CIDER? By default, CIDER passes the parameters repl -s wait
to Boot for cider-jack-in
. I could play to those defaults by mucking around with the repl
and wait
tasks:
(task-options!
repl {:init-ns 'user})
(replace-task!
[_ wait] #(comp (watch) (refresh :system 'user/sys)))
This does allow me to use C-c M-j
to instantly start up a fully reloadable system, but it feels far too ugly for me to use in good conscience.
The only other approach I can think of would be to customize cider-boot-parameters
by adding a .dir-locals.el
file to my project directory.
((nil . ((cider-boot-parameters . "repl -sn user watch refresh -s user/sys"))))
Of course, as soon as I do this, Emacs warns me that my local variables list contains values that may not be safe, and every time I open another buffer in my project directory, I have to type y
to apply the local variables list. After doing a bit of research, I found a way to disable these warnings, which I promptly added to my Emacs config:
(put 'cider-boot-parameters 'safe-local-variable #'stringp)
However, this again makes me feel uncomfortable. CIDER is going to be executing the contents of that variable. Who knows what some devious Clojure developer might stick in their cider-boot-parameters
? "&& touch file"
works as a proof of concept, but it could be something a whole lot worse. Also, assuming that I do choose to go with the .dir-locals.el
approach, should that file go in my .gitignore
or not?
Another problem is the repression of all the errors that will inevitably occur as I make typos in my source code, then reload the system. For instance, if I were to try to :refer :al
in one of my namespaces, I'd get no indication of my mistake until I went back to my REPL and tried to evaluate something:
user> (+ 1 1)
user>
user> |
In the *Messages*
buffer, CIDER helpfully tells me "Namespace not found.", but that's it. No stack trace or anything like that. I spent a while trying to come up with a way to have my refresh
task display an error message to me when an error occurs, but the best I could think of was the (throw x)
business you saw earlier. What I'd really like would be for the exception to pop up in CIDER's own stack trace viewer so I can quickly jump to the code that caused the error, but I just can't figure out a way to do that.
Thanks for taking the time to read all this. In summary, here are the questions for which I'm most in need of feedback:
- How should I keep my various source directories (e.g.
src
,test
,dev
) separate for different build tasks? - How should I customize my Boot or CIDER setup to allow me to use
cider-jack-in
properly? - How should I handle errors that occur while automatically reloading my source code?
Those are the things that puzzle me the most, but I'd really appreciate any feedback you have to offer on my workflow.