Skip to content

Boot for Leiningen Users

Teodor Heggelund edited this page Jan 27, 2018 · 21 revisions

All of the different abstractions used to control how Leiningen works (plugins, profiles, middleware, injections, etc.) can be implemented as boot tasks. This document shows some of the common patterns Leiningen users will be accustomed to and their boot equivalents.

Profiles, Middleware

The equivalent of Leiningen profiles and middleware in boot are tasks that modify the environment and return clojure.core/identity, the no-op task middleware.

For example, a task to add the test directory to the :source-paths when running tests, similar to the test profile in Leiningen:

(deftask testing
  "Profile setup for running tests."
  []
  (set-env! :source-paths #(conj % "test"))
  identity)

Profiles are "activated" simply by adding them to the pipeline, for example:

$ boot testing run-tests

This is equivalent to Lein's

$ lein with-profile testing run-tests

Environment Variables

In lein in order to pass a profile specific environment variable, it can be specified as a -D param in :jvm-opts:

:profiles {:dev {:jvm-opts ["-Dparam=value"]}}

While boot has several ways to set JVM Options, if all that is needed is -D, it can be easily done with a good old System/setProperty, since boot build configuration is just Clojure:

(deftask dev []
  (System/setProperty "param" "value")
  ;; ...)

It does not have to be a deftask, it can just be a defn that will be available in REPL. Or, if needed, (System/setProperty "param" "value") can be set globally (i.e. not task specific) per build.boot.

lein deps

In boot dependencies are added to the class path via the set-env! or merge-env! functions in the boot.core namespace. These functions are normally called in the build.boot script.

Fetching Dependencies

There is no task specifically for fetching dependencies. Running boot with no options will accomplish this as the build.boot file is evaluated. If you want to have a deps task that will fetch dependencies you can define one:

(deftask deps [])

REPL Dependencies

The repl task is different from most other tasks because the nREPL server can't be run in a pod (because you want to have a REPL in the project context, not a fresh pod context). So the repl task calls set-env! itself to add its nREPL dependencies at runtime when it is actually used, and the project has no nREPL dependencies when the repl task isn't used.

This means that the method described above will not fetch dependencies needed to start the REPL. This could be an issue when building Docker images, for example. The solution is to do something like this:

boot repl -s

or

(deftask deps [] (repl :server true))

lein deps :tree

In boot the show task can print the dependency graph:

boot show -d

You can also see dependency conflicts:

boot show -p

lein pprint

The equivalent of lein pprint in boot is:

boot show -e

lein run

The most trivial equivalent of lein run is a very simple task.

(require 'my.namespace)
(deftask run []
  (with-pass-thru _
    (my.namespace/-main)))

Obviously, you're not limited here to calling the -main function, and you can pick an arbitrary namespace. If you want to pass arguments to your main function, boot actually provides a nice benefit over lein by providing a very nice DSL for declaring task options. Thus you can now move your argument parsing code out of your library code and into the build.boot file. Of course that doesn't help you if you want to distribute a standalone jar that takes command-line arguments.

lein trampoline and boot pods

At first glance, it seems like lein trampoline and boot's pods mechanism serve a similar purpose. But really they serve very distinct purposes. lein trampoline is an optimization lein uses to cache your project's classpath so that the lein program can do less work before starting the project JVM.

Boot's pods mechanism on the other hand are intended to provide the classpath isolation that lein achieves by launching the project code in a separate JVM from the one lein itself is running in.

The difference is that boot pods are first-class and in-process. Since they're in the same process, and because they're first class you can have anonymous pods, you can put a pod in a future, etc. You can make functions that take pods as arguments and functions that return pods.

boot itself is a small java program distributed as an uberjar: boot.sh. This uberjar can be executed directly because we exploit a nice property of jar files - you can put random garbage at the front of a jar file and the jvm will still load it without problems. So we inject a shebang and a call to java -jar there.

The java program boot.sh provides a single class - boot.App. boot.App has static methods that provide a main() entry point for the jar, and methods to create new pods. Boot runs all clojure code in pods so your build.boot script is actually running in a pod but your pod can access boot.App and use it to create new pods that it can evaluate expressions in. All pods communicate with each other via boot.App.

IRC discussion -- (to be cleaned up)

AAAA: So far I've ported `lein uberjar` to a boot.build file. But I still don't know how to port `lein run` and `lein run -m my.non-main.namespace` and `lein speclj`. That's what's keeping me from switching from Leiningen to Boot.
AAAA: I understand that `lein spec` needs a third party plugin to be created.
AAAA: But it would be nice if the Boot website or FAQ or wiki had some instructions on porting `lein uberjar`, `lein run`, and `lein run -m some.custom.namespace` to Boot from Leiningen.
...
AAAA: As it is, these are blockers so that I can't switch to Boot yet, even though I see that it would be more helpful for me than Leiningen.
...
CCCC: AAAA: does 'boot uber jar' produce an uberjar for you similar to lein uberjar?
AAAA: CCCC: I've gotten the `lein uberjar` part figured out (the exact details vary depending on project requirements); it's the rest I still don't have yet
CCCC: i haven't used lein in a while, can you describe what lein run is supposed to do?
BBBB: AAAA: boot does not have any dependencies of its own
BBBB: AAAA: re: your question about boot's deps vs your project deps
BBBB: AAAA: that's the point of having pods, it serves a similar purpose to lein's second jvm
BBBB: but more granular and first-class
AAAA: BBBB: At the very least it depends on clojure.core
BBBB: AAAA: not really, it doesn't bring in a clojure dependency in your project
AAAA: BBBB: but only if you're using Pods, right? otherwise it still does, right?
BBBB: clojure.core is *provided* at runtime by boot, but that doesn't have any restriction on your project
BBBB: AAAA: just do `boot pom -p foo -v 1.0` and inspect the pom.xml it generates
BBBB: you'll see no dependencies at all
BBBB: so if your project needs a clojure dependency you need to add it
BBBB: AAAA: your question about uberjar and lein run is here: https://github.com/adzerk-oss/boot-uberjar-example
BBBB: it addresses both of them
BBBB: boot has an "uber" task that can be composed with any packaging task, like jar, war, zip, whatever
BBBB: we've decoupled those
BBBB: and lein run is just making your own task that runs something
BBBB: AAAA: for more info about what gets packaged in your uberjar i recommend looking at `boot uber -h`
AAAA: Great thanks.
BBBB: AAAA: it would be very awesome if you would update the wiki with any information you think is helpful 
BBBB: AAAA: https://github.com/boot-clj/boot/wiki/Boot-for-Leiningen-Users
BBBB: also, re: trampoline vs. pods
BBBB: the lein "trampoline" is not the same thing as pods at all
noprompt joined the chat room.
BBBB: trampoline is an optimization lein uses to cache classpath stuff
BBBB: running multiple jvms
BBBB: pods are first-class and in-process
BBBB: boot itself is a small java program
BBBB: distributed as an uberjar (boot.sh)
BBBB: this uberjar can be executed directly because we exploit a nice property of jar files---you can put random garbage at the front of a jar file and the jvm will still load it without problems
BBBB: so we inject a shebang and a call to java -jar there
BBBB: boot.sh, the java program, provides a single class, boot.App
BBBB: boot.App has static methods that provide a main() entry point for the jar, and methods to create new pods
BBBB: boot runs all clojure code in pods
BBBB: so your build.boot script is actually running in a po
BBBB: pod
BBBB: but your pod can access boot.App and use it to create new pods that it can evaluate expressions in
BBBB: all pods communicate with each other via boot.App
BBBB: this is very different from lein trampoline
BBBB: since they're in the same process, and becuase they're first class you can have anonymous pods, you can put a pod in a future, etc
BBBB: you can make functions that take pods as arguments and functions that return pods
Clone this wiki locally