Skip to content

Instantly share code, notes, and snippets.

@tolitius
Last active May 13, 2019 23:48
Show Gist options
  • Save tolitius/284c156e2dec4155d3f6 to your computer and use it in GitHub Desktop.
Save tolitius/284c156e2dec4155d3f6 to your computer and use it in GitHub Desktop.
Boot: Build it like a Pro(grammer)! http://www.meetup.com/Clojadelphia/events/229257264/

Thanks!

Alan Dipert (@alandipert) and Micha Niskin (@micha)

for Boot, slides and help with Boot intel.

Juno Terepi (@deraen)

for slides and help with Lein pitfals

Boot slack channel

for being always on and always helpful


and now, fasten your seatbelts, let's begin...

Lein ", but"s

+ other JVMs

## 3(!) JVMs
lein do cljsbuild, compile
JVM 1 JVM 2 JVM 3
lein cljsbuild compile

most lein commands would fork yet another JVM

so what?

  • slow startup
  • large memory footprint

Plugins are not easily composable

It is possible to pass data from task to task, but you need to usually manually set the paths for both tasks

  • "manually" ensure that task A's target-path is the same as task B's input-dir
  • each plugin has to reimplement file watching logic

"Always Be Cleaning"

single tmp dir called target => immutable? I think not.

anything weird is going on during development? first thing would be to:

lein clean

also don't forget to provide all the :clean-targets or clean them manually after lein clean

Customizing, Fixing and Understanding

In case there is:

  • an error in a lein plugin or
  • it needs customization
  • or just needs to be understood internally + some undocumented features, etc.

the lein plugin implementation is implicit: i.e. hidden behind the lein config DSL which makes it hard(er) to understand / debug / customize.


</rant>

Why I Love Boot

Piping tasks

from command line

$ boot pom -- jar -- install

Writing pom.xml and pom.properties...
Writing cprop-0.1.7-SNAPSHOT.jar...
Installing cprop-0.1.7-SNAPSHOT.jar...

add some visual in between:

$ boot show -f -- pom -- jar -- show -f -- install

cprop
├── core.clj
├── source.clj
└── tools.clj
Writing pom.xml and pom.properties...
Writing cprop-0.1.7-SNAPSHOT.jar...
META-INF
└── maven
    └── cprop
        └── cprop
            ├── pom.properties
            └── pom.xml
cprop
├── core.clj
├── source.clj
└── tools.clj
cprop-0.1.7-SNAPSHOT.jar
Installing cprop-0.1.7-SNAPSHOT.jar...

from REPL

boot.user=> (boot (pom) (jar) (install))

Writing pom.xml and pom.properties...
Writing cprop-0.1.7-SNAPSHOT.jar...
Installing cprop-0.1.7-SNAPSHOT.jar...
boot.user=> (boot (show "-f") (pom) (jar) (install))

config.edn
cprop
├── core.clj
├── source.clj
├── test
│   └── core.clj
└── tools.clj
fill-me-in.edn
Writing pom.xml and pom.properties...
Writing cprop-0.1.7-SNAPSHOT.jar...
Installing cprop-0.1.7-SNAPSHOT.jar...

good old Unix way

Process Connective
Unix Shell program text
Boot task FileSet

build.boot

walkthorugh a cprop's build.boot as an example

talk about:

  • env
  • composing tasks
  • boot libs
  • tasks args

cljs

walkthorugh a mount's build.boot as an example

talk about:

  • (cljs-repl) starts a cljs RPEL server
  • (reload) automatically reload resources in the browser when files in the project change
  • (serve) starts a webserver that will serve our compiled JS / CSS and anything else that is in "resources"
  • (test-cljs) composed custom task to run cljs tests

cljs repl

$ boot cljs-dev
$ boot repl -c

## start the Weasel server and attach this REPL client to running browser environment
boot.user=> (start-repl)

<< started Weasel server on ws://127.0.0.1:65190 >>
<< waiting for client to connect ... 
   Connection is ws://localhost:65190
   Writing boot_cljs_repl.cljs...
   connected! >>
   
To quit, type: :cljs/quit
nil
cljs.user=> (js/alert "boot: \"greetings clojuredelphians!\"")

test cljs

$ boot watch speak test-cljs

change something in test/cljs/... listen to the music of success

Building several artifacts from a single project

cljs packages example

Creating a simple task

walkthogh a simple boot-stripper task:

(deftask strip-deps-attr
  "strips out an attribute (optionally identified by a value) from all the dependencies"
  [a attr ATTR kw "the name of the attribute (i.e. \"classifier\") to strip out"
   v value VALUE edn "the optional value of the attribute to strip out"]

  (when-not attr
    (boot.util/fail "The -a/--attr option is required!\n") (*usage*))

  (set-env! :dependencies #(mapv (partial strip-out attr value) %))
  identity)

source

Pods

  • isolating dependencies into completely independent Clojure runtimes
  • pod's templates via backtick

examples from boot-check:

make-pod-pool

(let [pod-pool (make-pod-pool (concat pod-deps eastwood-deps) bootstrap)]
  (core/with-pre-wrap fileset
    (eastwood/check pod-pool fileset) ;; TODO with args
    fileset)))

source

pod/with-eval-in

(defn check [pod-pool fileset & args]
  (let [worker-pod (pod-pool :refresh)]
    (pod/with-eval-in worker-pod
      (require '[eastwood.lint :as eastwood])
      ;; ~(boot.core/load-data-readers!)
      (let [sources# #{~@(tmp-dir-paths fileset)}
            _ (boot.util/dbug (str "eastwood is about to look at: -- " sources# " --"))
            {:keys [some-warnings]} (eastwood/eastwood {:source-paths sources#
                                                        ;; :debug #{:ns}
                                                        })]
        (if some-warnings
          (boot.util/warn (str "\nWARN: eastwood found some problems ^^^ \n\n"))
          (boot.util/info "\nlatest report from eastwood.... [You Rock!]\n"))))))

source

Immutable filesets

see those :directories:

[cprop]$ boot show -e

{:watcher-debounce 10,
 :dependencies
 [[org.clojure/clojure "1.8.0"]
  [boot/core "2.5.1" :scope "provided"]
  [adzerk/bootlaces "0.1.13" :scope "test"]
  [adzerk/boot-test "1.0.6" :scope "test"]
  [tolitius/boot-check "0.1.1" :scope "test"]],
 :directories
 #{"/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/f0sqpx"
   "/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/mh5540"
   "/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/mm3x96"
   "/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/-rcsl8f"},
 :source-paths #{"src"},
 :resource-paths #{"src"},
 :asset-paths #{},
 :target-path "target",
 :repositories
 [["clojars" {:url "https://clojars.org/repo/"}]
  ["maven-central" {:url "https://repo1.maven.org/maven2"}]],
 :config
 {"BOOT_CLOJURE_NAME" "org.clojure/clojure", "BOOT_FILE" "build.boot", "BOOT_CLOJURE_VERSION" "1.7.0", "BOOT_VERSION" "2.5.5", "BOOT_EMIT_TARGET" "no"}}

isolated + immutable = reliable

Shell scripting

show-conf.sh

#!/usr/bin/env boot

(set-env! :dependencies '[[cprop "0.1.6"]])
(require '[clojure.pprint :refer [pprint]]
         '[cprop.core :refer [load-config]]
         '[cprop.source :refer [from-env]])

(defn -main [& args]
  (let [with-env? (some #{"+env"} (set args))
        env (if with-env? (from-env) {})]
    (pprint (load-config :file "config.edn"
                         :merge [{:args args} env]))))

with some args:

$ ./show-conf.sh foo bar baz
{:datomic
 {:url
  "datomic:sql://?jdbc:postgresql://localhost:5432/datomic?user=datomic&password=datomic"},
 :source
 {:account
  {:rabbit
   {:host "127.0.0.1",
    :port 5672,
    :vhost "/z-broker",
    :username "guest",
    :password "guest"}}},
 :answer 42,
 :args ("foo" "bar" "baz")}

with all ENV variables:

$ ./show-conf.clj +env

...

The big thing going for Boot CLI scripts are libraries such as those dealing with AWS, rabbitmq, etc, which are available in maven, and can be simply used in "boot" scripts.

Templating

show off boot-new

boot -d seancorfield/boot-new new -t app -n whatsapp

boot run

boot test

Debugging Dependencies

clean and simple:

# -p, --pedantic          Print graph of dependency conflicts.
$ boot show -p

[!] clj-time
    ✔ 0.9.0
      compojure
    ✘ 0.3.7
      ring/ring-jetty-adapter
[!] commons-codec
    ✘ 1.6
      compojure
      ring/ring-jetty-adapter
    ✔ 1.5
      com.datomic/datomic-free
[!] commons-fileupload
    ✔ 1.3.1
      compojure
    ✘ 1.2.1
      ring/ring-jetty-adapter
[!] commons-io
    ✔ 2.4
      compojure
    ✘ 2.1
      ring/ring-jetty-adapter
[!] joda-time
    ✔ 2.6
      compojure
    ✘ 2.0
      ring/ring-jetty-adapter
[!] com.fasterxml.jackson.core/jackson-core
    ✔ 2.5.3
      cheshire
    ✘ 2.3.2
      com.datomic/datomic-free
[✔] org.clojure/clojure
    ✘ 1.7.0-RC1
      weasel
    ✔ 1.7.0
      com.andrewmcveigh/cljs-time
      org.clojure/clojure
      org.clojure/clojurescript
    ✘ 1.6.0
      com.cemerick/piggieback
      com.datomic/datomic-free
    ✘ 1.5.1
      cheshire
      compojure
      hiccups
    ✘ 1.4.0
      org.clojure/tools.logging
      org.clojure/tools.namespace
    ✘ 1.2.1
      ring/ring-jetty-adapter
    ✘ 1.2.0
      org.clojure/tools.nrepl

Addressing Lein Pitfalls

Lein: Multiple JVMs

Boot always uses single JVM for all the work Boot + Tasks.

It uses Pods (separate classloaders) to prevent dependency hell and to isolate artifacts between tasks / runs.

Lein: Plugins are not easily composable

Boot tasks are functions. Functions compose.

Lein: "Always Be Cleaning"

Boot tasks create their own temporaty directories for artifacts when they are needed, i.e.

 :directories
 #{"/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/f0sqpx"
   "/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/mh5540"
   "/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/mm3x96"
   "/Users/you/.boot/cache/tmp/Users/you/fun/cprop/cp8/-rcsl8f"}

every time you run a task, you start from a clean set of artifacts

Lein: Customizing, Fixing and Understanding

Boot tasks are functions. boot.build is Clojure code.

Customizing / fixing / understanding a task is no different than doing it for any other Clojure library.

There is no mental gap of decyphering someone's DSL.


</done>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment