読者です 読者をやめる 読者になる 読者になる

(define -ayalog '())

括弧に魅せられて道を外した名前のないプログラマ

Boot を使いはじめる(インストールから基本的な使い方まで)

Clojure

一個前の記事で言及している Boot についてです。
f:id:ayato0211:20150501175222p:plain
github.com

ああ、ロゴが可愛いですね。本当に。

さて、さくさくっと 1 週間くらい触ってきて知見もそこそこにたまってきたので公開します。

目次

思いの外チュートリアルが長くなったので…

  • インストールの仕方
  • 簡単なチュートリアル
  • 幾つかの TIPS
  • 僕が Boot を使い始めてよかったと思った点

インストールの仕方

*nix/Mac 系なら boot.sh をダウンロードして次のようなコマンドを実行

$ mv boot.sh boot && chmod a+x boot && sudo mv boot /usr/local/bin

Windows なら*1 boot.exe をダウンロードして

C:\> move boot.exe C:\Windows\System32

Boot を一度実行すると Leiningen と同じように色々ダウンロードしたりして起動するための準備が始まります。
Install - Boot

実際にインストールしたあとに軽く動作確認をしましょう。とりあえず、最初に以下のコマンドを打ってヘルプを表示してみます。

$ boot -h

幾つかのオプションとビルトインなタスクの一覧が表示されると思います。各種タスクに関してもヘルプも同様にして参照できます。

$ boot repl -h

REPL の中でも Boot の各種タスクのヘルプを参照できます。以下のコマンドで REPL を起動します。

$ boot repl
boot.user=> (doc repl)

少しだけコマンドラインとは表示の仕方が違いますが、これは REPL やビルドスクリプトでの使い方なのでちょっと違うんですよね。

簡単なチュートリアル

ちょっと長くなりましたが、 Boot の README をカジュアルに訳したくらいのノリなんで、 README 読んだ人には同じことの繰り返しになるので読まないでいいです。

コマンドラインから実際にビルドをしてみる感じです。以下のコマンドを叩きます。

$ mkdir -p my-project/src
$ cd my-project
$ echo "hi there" > src/hello.txt

するとこういうプロジェクトが出来上がっていると思います。

.-(~/projects/my-project)
`--> tree
.
└── src
    └── hello.txt

1 directory, 1 file

さて、今から jar ファイルをちょっとビルドして Maven のローカルリポジトリにインストールしたいと思います。 pom, jar と install のタスクを組み合わせてコマンドラインから実行します。

# `--` 引数はオプションですが、タスク間を視覚的に切り分けるのに便利なので使っています。
$ boot -s src -d me.raynes/conch:0.8.0 -- pom -p my-project -v 0.1.0 -- jar -M Foo=bar -- install

このコマンドの中で何をやっているかを説明すると

  • boot コマンドの `-s` 引数でソースのディレクトリを指定しています
  • boot コマンドの `-d` 引数で conch を依存性に含めています

ここまででビルド環境のセットアップを行っています。ここからは実際のビルドです。

  • pom タスクでプロジェクト ID とバージョンを指定します
  • jar タスクでマニフェストの Foo キーに bar を指定してビルドします
  • 最後に install タスクを引数なしで実行してローカルリポジトリへとインストールします

my-project-0.1.0.jar は target ディレクトリに作成されますと。なんとなく便利そうですよね。

同様のことを REPL で実行してみましょう。

# 個人的に REPL の出力デフォルトでカラーリングされているんですが、見にくいので boot -C repl としてカラーリングをオフにするのを推奨します。
$ boot repl 

で REPL を起動します。

boot.user=> (set-env! 
       #_=>   :source-paths #{"src"}
       #_=>   :dependencies '[[me.raynes/conch "0.8.0"]])
boot.user=> (boot (pom :project 'my-project :version "0.1.0")
       #_=>       (jar :manifest {"Foo" "bar"})
       #_=>       (install))

とすると上で説明したのと同様の結果を得ることができます。
set-env! でランタイム環境に対してプロジェクトで必要なものをセットすることが出来て、それは key & value の形でいつも通りの map like な設定の仕方が出来ます。
boot 関数への引数は各種タスクを実行して返ってきた関数です。まぁ boot 関数自体は comp 関数みたいなものですが、引数に渡された順番でタスクが実行されるくらいの認識を持っていればいいと思います。

さて、今のタスクの実行部分を見てみると引数をまだ直接指定しています。今のまま使うのならこれでもいいかもしれませんが、例えば dev, prod で同じようなものを実行するけど、一部だけ変えたいというときに同じことを 2 回書くのは嫌ですよね。なので Boot は共通的な設定をグローバルに設定する関数を提供しています。

boot.user=> (task-options!
       #_=>   pom {:project 'my-project
       #_=>        :version "0.1.0"}
       #_=>   jar {:manifest {"Foo" "bar"}})

task-options! という関数で pom, jar タスクのそれぞれに共通で設定したいものを書きます。すると元々の boot 関数はこう書くことができます。

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

綺麗ですね。一部だけ実行時に設定を変えたい場合は次のように書けます。

boot.user=> (boot (pom :version "0.1.1") (jar) (install))

とてもシンプルですよね? Boot のビルドスクリプトは最後の引数が勝つのでこのように書くことができます。

さぁ、ここまでは非常にシンプルに出来たので良かったと思います。ですが、いちいちコマンドラインに同じコマンドを打ち込んだり REPL に打つのは大変です。なのでビルドスクリプトを書きます。
Boot では build.boot というファイルをプロジェクトディレクトリ直下に置くことでその設定を boot 起動時に読み込むことが出来ます。実際に以下のような設定を書いた build.boot を書いてみましょう。

(set-env!
  :source-paths #{"src"}
  :dependencies '[[me.raynes/conch "0.8.0"]])

(task-options!
  pom {:project 'my-project
       :version "0.1.0"}
  jar {:manifest {"Foo" "bar"}})

これで僕らは特別な引数を boot 関数に渡すことなくコマンドラインから実行することが出来ます。

$ boot pom jar install

もちろん、実行時にオプションを上書きすることもできます。

$ boot -- pom -v 0.1.1 -- jar -- install

気づいていると思いますが (boot ...) の部分は build.boot には必要ありません。 Boot はランタイム環境をコマンド引数と boot のスクリプト*2から構築します。
なので今 REPL を boot スクリプトのコンテキスト*3で実行すると次のように REPL 上で実行することが出来ます。

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

さらに発展させた話題として Boot ではユーザーが自由にプロジェクトや build.boot の中でタスクを定義出来るます。実際にはこれが一般的な Boot の使い方になります。
今までのタスクを連結させたタスクを build という名前で build.boot に定義してみると以下のようになります。

(set-env!
  :source-paths #{"src"}
  :dependencies '[[me.raynes/conch "0.8.0"]])

(task-options!
  pom {:project 'my-project
       :version "0.1.0"}
  jar {:manifest {"Foo" "bar"}})

(deftask build
  "Build my project."
  []
  (comp (pom) (jar) (install)))

タスクを定義するのには deftask というマクロを使います。こうすることでこのコンテキストで boot -h を実行すると build タスクがタスクの一覧の中に含められていることが分かります。そして、僕らはこれを今コマンドラインから直接実行することが出来ます。

$ boot build

ざっとこんな感じで boot を使うことが出来ます。比較的シンプルだし、多少サンプルを読めば簡単に買えそうだなーというのがわかってもらえたと思います。

幾つかの TIPS

さてここまでで、基本的な使い方をカバーしたと思うので、もう少し踏み込んだ設定について書いてみます。

ユーザーレベルで全てのプロジェクトに依存させたいものは何処に書くべき?

例えば、 cider ようの設定とか。これは $BOOT_HOME/profile.boot に書くようになっています。 $BOOT_HOME 環境変数を設定していなければ、 ~/.boot がデフォルトです。

Cider ユーザーのための設定はどう書くの?

Wiki にも書いてありますが、次のような定義を profile.boot に書きます。

(require 'boot.repl)
(swap! boot.repl/*default-dependencies*
       concat '[[cider/cider-nrepl "0.8.2"]])

(swap! boot.repl/*default-middleware*
       conj 'cider.nrepl/cider-middleware)

ref Cider REPL - Boot
Cider は既に Boot にも対応しているので build.boot が存在すれば自動的に boot を起動しようとします。 build.boot と project.clj が同時に存在する場合、一度どっちを起動するか聞かれます(デフォルトは Leiningen )。

spyscope みたいなのはどうやって使ったらいいの?

僕は次のような定義を profile.boot に定義しています。

(merge-env! :dependencies '[[spyscope "0.1.5" :scope "test"]])
(require 'spyscope.core)
(load-data-readers!)

load-data-readers! は比較的最近作られた関数なんですが、 Clojure 環境の起動時にしか読み込まれない read-macro(Tagged Literals) をもう一度読み込むようです。これによって #spy/p などが Leiningen のとき同様に使えます。

今まで Leiningen で使ってたプラグインとかの代替はある?

あるものもあるし、ないものもあると思います。
Community Tasks - Boot
ないものもあると思いますけど、自分で作ってしまってもいいと思います。ただ、気をつけて欲しいのは Leiningen のプラグインと Boot のタスクは思想が違うので、一度本当に必要か考えた方が良さそうです。

プロジェクトの Clojure のバージョンとか Boot のバージョンを固定したい場合はどうしたらいい?

これはプロジェクト配下に boot.properties というのを作成します。
作り方は簡単で以下のコマンドを叩くだけ。

$ boot -V > boot.properties
プロジェクトのリロードはどうしたらいいですか?

一応 REPL の中で次のようにすればリロードできます。

boot.user=> (require '[clojure.tools.namespace.repl :as repl])
boot.user=> (def dirs (get-env :directories)) ;; "src" とかを直接送ってもいい気がします。
boot.user=> (apply repl/set-refresh-dirs dirs)

ref Repl reloading · boot-clj/boot Wiki · GitHub
これはタスクとかとの兼ね合いもあるので実際にはこっちを見た方が参考になるかもしれません。Boot を利用したちいさい Web アプリのテンプレートですがリローダブルになっているので是非読んでみるといいかと思います。

inf-clojure ユーザーなんだけど、 boot どうやって使ったらいいの?

次のように inf-clojure-program 変数を上書きしたらいいと思います。

(setq inf-clojure-program "boot repl")
boot スクリプトで何がどういう順番でロードされたりしているか知りたいんだけど?

boot コマンドの `-b` オプションが役に立つはずなので是非使ってください。例えば saapas のコンテキストで実行すると以下のような結果が出てきます。

.-(~/projects/saapas)
`--> boot -b repl
(ns boot.user (:use boot.core boot.util boot.task.built-in))

(clojure.core/comment "start boot script")

(set-env! :source-paths #{"src/less" "src/cljx" "src/cljs"} :resource-paths #{"src/clj"} :dependencies (quote [[adzerk/boot-cljs "0.0-2814-1"] [adzerk/boot-cljs-repl "0.1.9" :scope "test"] [adzerk/boot-reload "0.2.4" :scope "test"] [deraen/boot-cljx "0.2.2"] [deraen/boot-less "0.2.1"] [compojure "1.3.2"] [hiccup "1.0.5"] [http-kit "2.1.19"] [metosin/ring-http-response "0.5.2"] [org.clojure/clojure "1.6.0"] [org.clojure/tools.namespace "0.2.9"] [ring "1.3.2"] [org.clojure/clojurescript "0.0-3211"] [org.omcljs/om "0.8.8"] [prismatic/om-tools "0.3.10"] [sablono "0.3.4"] [org.webjars/bootstrap "3.3.2"]]))

(require (quote [adzerk.boot-cljs :refer :all]) (quote [adzerk.boot-cljs-repl :refer :all]) (quote [adzerk.boot-reload :refer :all]) (quote [deraen.boot-cljx :refer :all]) (quote [deraen.boot-less :refer :all]) (quote [saapas.boot :refer :all]))

(task-options! pom {:project (quote saapas), :version "0.1.0-SNAPSHOT", :description "Application template for Cljs/Om with live reloading, using Boot.", :license {"The MIT License (MIT)" "http://opensource.org/licenses/mit-license.php"}} aot {:namespace #{(quote saapas.main)}} jar {:main (quote saapas.main)} cljs {:source-map true} less {:source-map true})

(deftask dev "Start the dev env..." [s speak bool "Notify when build is done" r clj-reload bool "Use r.m.reload to reload changed clj namespaces on each request" p port PORT int "Port for web server"] (comp (watch) (reload :on-jsload (quote saapas.core/start!)) (less) (cljx) (cljs-repl) (cljs :optimizations :none :unified-mode true) (if speak (boot.task.built-in/speak) identity) (start-app :port port :reload clj-reload)))

(deftask package "Build the package" [] (comp (less :compression true) (cljx) (cljs :optimizations :advanced) (aot) (pom) (uber) (jar)))

(clojure.core/comment "end boot script")

(clojure.core/let [boot?__874__auto__ true] (clojure.core/if-not boot?__874__auto__ (clojure.core/when-let [main__875__auto__ (clojure.core/resolve (quote boot.user/-main))] (main__875__auto__ "repl")) (boot.core/boot "repl")))
Boot の起動を早くしたい

僕はこういう JVM オプションを設定しています。

`--> echo $BOOT_JVM_OPTIONS 
-Xmx4g -client -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -Xverify:none

一応 Wiki にも目を通すと良いと思います。 See -> JVM Options - Boot

僕が Boot を使い始めてよかったと思った点

起動がとにかく速いです。 5 秒くらいですっと上がってくる感じがいいです。これは Cider 用の nREPL を僕が inf-clojure に移ったタイミングで捨てたからというのも大きく寄与していると思っています*4が、とりあえず現状 cider-nrepl を捨ててつらいなーっていうのは今のところないですね。
あとは Chestnut の project.clj を読むよりは圧倒的にマシな設定ファイルを手に入れたことでしょうか。もちろん Leiningen と若干違うので勝手が違うところはあり多少の不便は最初ありましたけど、なんだかんだでそんなに問題にならないかなと思いました。
それから個人的に Boot と inf-clojure の親和性高いというか、無駄なものを削ぎ落した感じがあってこの組み合わせ好きです。もちろん Cider ユーザーでも Boot 使えるんでオススメしますけど。

長くなりましたが、あやぴー的には Boot オススメです。

*1: 1 系のときはサポートしてなかったらしいけど、最近はサポートしているらしい

*2:not only build.boot

*3:つまり、プロジェクト配下

*4:JVM オプションも設定してるけどね