Boot is a shell interpreter for scripts written in Clojure and a Clojure build environment.
It can be used with the “shebang” style of shell scripts to provide a simple means to have single file, self-contained scripts in Clojure that can have dependencies on Maven artifacts but aren't part of a project or uberjar.
The boot build environment provides facilities to manage any Clojure build process a programmer can imagine.
FIXME: How to doing?
To get started let's make a Hello World script (of course). Create a file named
hello.boot
(boot scripts must have the .boot
extension):
#!/usr/bin/env boot
(defn -main [& args]
(println "hello, world!"))
Make the script executable:
$ chmod a+x hello.boot
Now you can run it:
$ ./hello.boot
hello, world
Good job dude!
Scripts can add Maven dependencies and/or directories to the class path at
runtime using set-env!
, like this:
#!/usr/bin/env boot
(set-env!
:repositories #{"http://me.com/maven-repo"}
:dependencies '[[com.hello/foo "0.1.0"]])
(require '[com.hello.foo :as foo])
(defn -main [& args]
(println (foo/do-stuff args))
(System/exit 0))
In addition to interpreting scripts, boot also provides some facilities to help
build Clojure projects. Omitting the -main
function definition puts boot into
build tool mode.
Create a minimal build.boot
file containing only the shebang and core version:
$ boot :strap > build.boot
The resulting file should contain something like this:
#!/usr/bin/env boot
#tailrecursion.boot.core/version "2.0.0"
Then run it. You should see version and usage info and a list of available tasks:
$ boot
tailrecursion/boot 1.0.0: http://github.com/tailrecursion/boot
Usage: boot OPTS task ...
boot OPTS [task arg arg] ...
boot OPTS [help task]
OPTS: :v Verbose exceptions (full cause trace).
[:v n] Cause trace limited to `n` elements each.
Tasks: debug Print the value of a boot environment key.
help Print help and usage info for a task.
lein Run a leiningen task with a generated `project.clj`.
repl Launch nrepl in the project.
syncdir Copy/sync files between directories.
watch Watch `:src-paths` and call its continuation when files change.
Create a minimal boot script: `boot :strap > build.boot`
The tasks listed in the output are defined in the core tasks namespace,
which is referred into the script namespace automatically. Any tasks defined or
referred into the script namespace will be displayed in the list of available
tasks printed by the default help
task.
Notice that when the boot script file is named
build.boot
and located is in the current directory you can callboot
directly instead of executing the boot script file itself. This is more familiar to users of Leiningen or GNU Make, for example, and reinforces build repeatability by standardizing the build script filename and location in the project directory.
Let's create a task to print a friendly greeting to the terminal. Modify the
build.boot
file to contain the following:
#!/usr/bin/env boot
#tailrecursion.boot.core/version "2.0.0"
(deftask hello
"Print a friendly greeting."
[& [name]]
(fn [continue]
(fn [event]
(printf "hello, %s!\n" (or name "world"))
(continue event))))
Run it again to see the new task listed among the other available tasks:
$ boot
tailrecursion/boot 1.0.0: http://github.com/tailrecursion/boot
Usage: boot OPTS task ...
boot OPTS [task arg arg] ...
boot OPTS [help task]
OPTS: :v Verbose exceptions (full cause trace).
[:v n] Cause trace limited to `n` elements each.
Tasks: debug Print the value of a boot environment key.
hello Print a friendly greeting.
help Print help and usage info for a task.
lein Run a leiningen task with a generated `project.clj`.
repl Launch nrepl in the project.
syncdir Copy/sync files between directories.
watch Watch `:src-paths` and call its continuation when files change.
Create a minimal boot script: `boot :strap > build.boot`
Now we can run the hello
task:
$ boot hello
hello, world!
An argument can be passed to the hello
task like this:
$ boot \(hello :foo\)
hello, :foo!
The command line is read as Clojure forms, but task expressions can be enclosed in square brackets (optionally) to avoid having to escape parens in the shell, like this:
$ boot [hello :foo]
hello, :foo!
Tasks can be composed on the command line by specifying them one after the other, like this:
$ boot [hello :foo] [hello :bar]
hello, :foo!
hello, :bar!
Because tasks return middleware functions they can be composed uniformly, and
the product of the composition of two task middleware functions is itself a
task middleware function. The two instances of the hello
task above are being
combined by boot something like this:
;; [& args] command line argument list
("[hello" ":foo]" "[hello" ":bar]")
;; string/join with " " and read-string
=> ([hello :foo] [hello :bar])
;; convert top-level vectors to lists
=> ((hello :foo) (hello :bar))
;; compose with comp when more than one
=> (comp (hello :foo) (hello :bar))
This yields a middleware function that is called by boot to actually perform the build process. The composition of middleware sets up the pipeline of tasks that will participate in the build. The actual handler at the bottom of the middleware stack is provided by boot–it syncs artifacts between temporary staging directories (more on these later) and output/target directories.
Here we create a new named task in the project boot script by composing other
tasks. This is a quick way to fix options and simplify documenting the build
procedures. Tasks are functions that return middleware, and middleware are
functions that can be composed uniformly, so a task can compose other tasks the
same way as on the command line: with the comp
function.
Modify the build.boot
file such that it contains the following:
#!/usr/bin/env boot
#tailrecursion.boot.core/version "2.0.0"
(deftask hello
"Print a friendly greeting."
[& [name]]
(fn [continue]
(fn [event]
(printf "hello, %s!\n" (or name "world"))
(continue event))))
(deftask hellos
"Print two friendly greetings."
[]
(comp (hello :foo) (hello :bar)))
Now run the new hellos
task, which composes two instances of the hello
task
with different arguments to the constructor:
$ boot hellos
hello, :foo!
hello, :bar!
The global build environment contains the project metadata. This includes things
like the project group and artifact ID, version string, dependencies, etc. The
environment is accessible throughout the build process via the get-env
and
set-env!
functions.
For example:
#!/usr/bin/env boot
#tailrecursion.boot.core/version "2.0.0"
(set-env!
:project 'com.me/my-project
:version "0.1.0-SNAPSHOT"
:description "My Clojure project."
:url "http://me.com/projects/my-project"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies '[[tailrecursion/boot.task "2.0.0"]
[tailrecursion/hoplon "5.0.0"]]
:src-paths #{"src"})
(deftask env-value
"Print the value associated with `key` in the build environment."
[key]
(fn [continue]
(fn [event]
(prn (get-env key))
(continue event))))
In the example above the environment is configured using set-env!
and a task
is defined to print the environment value associated with a given key using
get-env
. (This task is similar to the core debug
task that is included in
boot already.) We can run the task like this:
$ boot [env-value :src-paths]
#{"src"}
Tasks defined in the build.boot
script can dynamically modify the build
environment at runtime. That is, they can use set-env!
to add dependencies or
directories to the classpath or otherwise update values in the build
environment. This makes it possible to define "profile" tasks that can be used
to modify the behavior of other tasks. These profile-type tasks can either
create a middleware function or simply return Clojure's identity
to pass
control directly to the next task.
For example:
#!/usr/bin/env boot
#tailrecursion.boot.core/version "2.0.0"
(set-env!
:project 'com.me/my-project
:version "0.1.0-SNAPSHOT"
:description "My Clojure project."
:url "http://me.com/projects/my-project"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies '[[tailrecursion/boot.task "2.0.0"]
[tailrecursion/hoplon "5.0.0"]]
:src-paths #{"src"})
(deftask env-mod
"Example profile-type task."
[]
(set-env! :description "My TEST Clojure project.")
identity)
(deftask env-value
"Print the value associated with `key` in the build environment."
[key]
(fn [continue]
(fn [event]
(prn (get-env key))
(continue event))))
Now, running this build.boot
script produces the following:
$ boot [env-value :description]
"My Clojure project."
$ boot env-mod [env-value :description]
"My TEST Clojure project."
In the build script the deftask
macro defines a function whose body is
compiled lazily at runtime when the function is called. This means that inside
a deftask
you can add dependencies and require namespaces which will then
be available for use in the build script.
For example:
#!/usr/bin/env boot
#tailrecursion.boot.core/version "2.0.0"
(set-env!
:project 'com.me/my-project
:version "0.1.0-SNAPSHOT"
:description "My Clojure project."
:url "http://me.com/projects/my-project"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:src-paths #{"src"})
(deftask load-hoplon
"Example profile-type task."
[]
(set-env!
:dependencies '[[tailrecursion/boot.task "2.0.0"]
[tailrecursion/hoplon "5.0.0"]])
(require '[tailrecursion.hoplon.boot :as h])
identity)
The load-hoplon
task adds the dependencies needed for building a Hoplon
application and requires the hoplon boot task namespace, aliasing it to h
locally. To see the effect run the build.boot
script with and without this
task and see how the list of available tasks changes.
First without the load-hoplon
profile:
$ boot help
tailrecursion/boot 1.0.0: http://github.com/tailrecursion/boot
Usage: boot OPTS task ...
boot OPTS [task arg arg] ...
boot OPTS [help task]
OPTS: :v Verbose exceptions (full cause trace).
[:v n] Cause trace limited to `n` elements each.
Tasks: debug Print the value of a boot environment key.
help Print help and usage info for a task.
lein Run a leiningen task with a generated `project.clj`.
load-hoplon Example profile-type task.
repl Launch nrepl in the project.
syncdir Copy/sync files between directories.
watch Watch `:src-paths` and call its continuation when files change.
Create a minimal boot script: `boot :strap > build.boot`
Then with the load-hoplon
profile:
$ boot load-hoplon help
tailrecursion/boot 1.0.0: http://github.com/tailrecursion/boot
Usage: boot OPTS task ...
boot OPTS [task arg arg] ...
boot OPTS [help task]
OPTS: :v Verbose exceptions (full cause trace).
[:v n] Cause trace limited to `n` elements each.
Tasks: debug Print the value of a boot environment key.
help Print help and usage info for a task.
lein Run a leiningen task with a generated `project.clj`.
load-hoplon Example profile-type task.
repl Launch nrepl in the project.
syncdir Copy/sync files between directories.
watch Watch `:src-paths` and call its continuation when files change.
h/hoplon Build Hoplon web application.
h/html2cljs Convert file from html syntax to cljs syntax.
Create a minimal boot script: `boot :strap > build.boot`
Notice how the second list includes h/hoplon
and h/html2cljs
, the two tasks
defined using deftask
in the Hoplon boot task namespace. You could run
the hoplon
task, for example, by doing
$ boot load-hoplon h/hoplon
The Java/Clojure build process is pretty much wedded to files in the filesystem. This adds incidental complexity to the build process and causes undesired coupling between tasks and between tasks and the project environment. Boot provides facilities to mitigate the issues with managing the files created during the build process. This allows tasks to be more general and easily composed, and eliminates configuration boilerplate in the project environment.
Tasks produce files which may be further processed by other tasks or emitted into the final output directory as artifacts. Using boot's file management facilities eliminates the need for the task itself to know which is the case during a particular build.
Boot's file management facilities eliminate the coupling between tasks and the filesystem, improving the ability to compose these tasks.
Boot manages these files in such a way as to never accumulate stale or garbage files, so there is no need for a "clean" task. This greatly simplifies the state model for the build process, making it easier to understand what's going on during the build and the interactions between tasks.
The boot build process deals with six types of directories–two of which are
specified in the project's boot environment (in the build.boot
file) and the
other four are created by tasks during the build process and managed by boot.
These directories contain files that are part of the project itself and are read-only as far as boot tasks are concerned.
Project source directories. These are specified in the :src-paths
key
of the boot environment for the project, and boot adds them to the project's
class path automatically.
Resource directories. These are specified using the add-sync!
function
in the build.boot
file. The contents of these directories are overlayed on
some other directory (usually the :out-path
dir, but it could be any
directory) after each build cycle. These directories contain things like CSS
stylesheets, image files, etc. Boot does not automatically add resource
directories to the project's class path.
These directories contain intermediate files created by boot tasks and are managed by boot. Boot deletes managed directories created by previous builds each time it starts.
Project output directory. This is specified in the :out-path
key of
the project boot environment. This is where the final artifacts produced by
the entire build process are placed. This directory is kept up to date and
free of stale artifacts by boot, automatically. Tasks should not add files
directly to this directory or manipulate the files it contains. Instead,
tasks emit artifacts to staging directories (see below) and boot takes care
of syncing them to the output directory at the end of each build cycle.
Generated source directories. These directories are created by tasks
via the mksrcdir!
function. Generated source dirs are similar to the project
source dirs, except that tasks can write to them and they're managed by boot.
Tasks can use these directories as a place to put intermediate source files
that are generated from sources in JAR dependencies (i.e. once created these
files won't change from one build cycle to the next).
Temporary directories. Temp directories are created by tasks via the
mktmp!
function. Tasks can use these directories for storing intermediate
files that will not be used as input for other tasks or as final compiled
artifacts (intermediate JavaScript namespaces created by the Google Closure
compiler, for instance). These directories are not automatically added to the
project's class path.
Staging directories. These directories are created by tasks via the
mkoutdir!
function. Tasks emit artifacts into these staging directories
which are cleaned automatically by boot at the start of each build cycle.
Staging directories are automatically added to the project's class path so
the files emitted there can be used as input for other tasks (or not) as
required. Files in staging directories at the end of the build cycle which
have not been consumed by another task (see below) will be synced to the
output directory after all tasks in the cycle have been run.
The image above illustrates the flow of files through the boot build process. On the left and right sides of the image are the various directories involved in the build process. The build process depicted consists of two tasks, "Task 1" and "Task 2", colored orange and red, respectively, displayed in the center of the image.
Tasks participate in the three phases of the build cycle: init, build, and filter. The initialization phase occurs once per boot invocation for each task, when the tasks are constructed. Tasks return middleware functions which handle the build phase of the process. Tasks may "consume" source files (see the next section). These files are removed from the staging directories of all tasks by boot during the filter phase of the build cycle.
After the final phase of the build cycle stale artifacts are removed from the project output directory and any artifacts that remain in staging directories are synced over to it.
FIXME
Artifacts are published on Clojars.
Copyright © 2013 Alan Dipert and Micha Niskin
Distributed under the Eclipse Public License, the same as Clojure.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close