How to manage leiningen profiles

Leiningen の profiles をどのように利用すればいいのか、あまり日本語だと情報がないので Leiningen のドキュメントをベースに噛み砕いて説明していきたいと思います(書き終わってみると半翻訳みたいになってしまったので、素直に翻訳にすればよかったかなーとか思ったり思わなかったり)。またこの記事はドキュメントをベースにしていますが、最後の方にちょっとした応用的な話を書いているので Leiningen の profiles にあまり詳しくない方は読むとためになるかもしれません。

参照: Profiles / Leiningen

Leiningen Profiles とは

普段、 Clojure を書いていると色々な Leiningen プラグインを利用したくなります。例えば cljfmt, ancient, eftest など。この中で cljfmt や ancient などはどちらかというとプロジェクトを横断して適用したいですし、 eftest は任意のプロジェクトだけで利用したいかもしれません。またプラグインでなくても、テストや開発中のときだけ利用したいソースコードやリソースというものはあります。逆にプロダクション用のビルドをするときに設定を有効にしたいもの( cljs の advanced コンパイルなど)も往々にしてあるでしょう。

Leiningen の profiles はこれらの問題を簡単に解決することができるようになっています。タスク実行時に有効な各 profiles が project.clj を元に作られるプロジェクトマップにマージされるため、有効になる profiles を制御することによって依存関係を追加したり、プラグインを追加することができるようになっています。

(defproject myproject "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :dependencies [[org.clojure/clojure "1.8.0"]]
  :profiles {:dev {:resource-paths ["test-data"]
                   :plugins [[lein-eftest "0.3.1"]]}
             :prod {:dependencies [[clojurescript "1.9.671"]]}})

つまり、このような project.clj があった場合、 dev profile が有効になるシーンでは test-data ディレクトリがリソースパスと読み込まれ、 lein-eftest がプラグインとして利用できるようになります。

$ lein show-profiles とすると、そのプロジェクトで利用することができる profiles の一覧が表示されます。何も設定していない場合、デフォルトで用意されている profiles しか表示されませんが、この一覧に表示された profiles 以外にもユーザーは自由に profiles を定義することができます(例えば :my-dev など)。

ということで詳しく見ていきましょう。

profiles の宣言方法

まずはどのように各 profiles を宣言するのか実際に例を見てみます。

:profiles {:dev {:resource-paths ["test-data"]
                 :plugins [[lein-eftest "0.3.1"]]}
           :prod {:dependencies [[clojurescript "1.9.671"]]}}

これは project.clj:profiles 部分だけを取り出してみたものです。 :dev:prod というのが profiles の名前です。各 profiles は project.clj の作るプロジェクトマップに設定することができるキーや、そのプロジェクト固有で必要なキーなどを設定することができます(つまり実質的に制限がない)。

先の例では dev profile と prod profile を宣言していました。つまり、 dev profile が有効になるときプロジェクトマップの :resource-paths:plugins は次のようになります。

{:resource-paths ["/home/ayato-p/projects/myproject/test-data"
                  "/home/ayato-p/projects/myproject/resources"]
 :plugins [[lein-eftest/lein-eftest "0.3.1"]]}

同様にしてprod profile が有効になる場合のプロジェクトマップの :dependencies は次のようになります。

{:dependencies [[org.clojure/clojure "1.8.0"]
                [clojurescript/clojurescript "1.9.671"]]}

おおよそ、雰囲気が掴めてきたと思います。次に project.clj:profiles キー以外に profiles を定義できるところを紹介していきましょう。最初はプロジェクトルートの profiles.clj です。

profiles.clj には、 project.clj:profiles キーに渡していたマップをそのまま書けばよいので、次のように書くことができます。

{:dev {:resource-paths ["my-test-data"]
       :plugins [[lein-eftest "0.3.1"]]}}

profiles.cljproject.clj にある各 profiles を、 profiles.clj にある同じ名前の profiles で上書きする効果があります。この場合だと profiles.clj に dev profile が存在したため、 project.clj のそれを上書きしてしまった形になります。注意しないといけないのはこのとき、上書きしたかったのは :resource-paths だけ なのですが、 :plugins も書いておかないと消えてしまうということです。ちなみに他の profiles は定義していないので、例えば prod profile は project.clj のまま利用されます。

このプロジェクトルートに用意する profiles.clj は、プロジェクト固有のもので project.clj を変更してコミットしたくはないけど、自分はこのプロジェクトのコードを書くときにこれを設定しておきたい、という場合などに使うと便利です。

次にプロジェクトをまたいで利用したい設定がある場合、ユーザーワイドな設定ファイルである ~/.lein/profiles.clj に記述することができます。これを記述しておくことによって全てのプロジェクトで適用したい profiles を管理しておくことができます。ただし、これらは同様の名前の profiles が各プロジェクトに存在する場合に上書きされるので注意が必要です。またシステムワイドな設定がある場合は /etc/leiningen/profiles.clj に記述することができます。ユーザーワイドな設定と同様ですが、優先順位はユーザーワイドより低いです。

また、ユーザーワイドな設定は ~/.lein/profiles.d/ ディレクトリ以下に clj ファイルとして保存することも出来ます。このとき、ファイルから .clj の拡張子を除いた部分が profile の名前となり、ファイルにはその profile として定義したい部分のマップのみを記述します。例えば my_dev profile というのを定義したい場合、 ~/.lein/profiles.d/my_dev.clj というファイルを作り次のように設定を記述することができます。

{:plugins [[lein-kibit "0.1.5"]]}

実際に利用できるようになった profile(s) は $ lein show-profiles で確認できるので、確認してみると my_dev というのが追加されていることが分かります。

profiles の合成

それぞれの profiles 間で同様の設定が必要なときに、 profile を共有したりすることができると便利なときがあります。これにはマップの代わりにベクタを使います。

{:shared {:port 5432 :database-name "mydb"}
 :qa [:shared {:server-name "qa-server.acme.com"}]
 :stage [:shared {:server-name "stage-server.acme.com"}]
 :production [:shared {:server-name "prod-server.acme.com"}]}

このとき、例えば qa profile は shared profile の定義とベクタの2つ目にあるマップを合成して qa profile とします。このときのマージ規則については後述する通りです。

デフォルトの profiles

with-profile タスクを利用しない場合、幾つかの profiles がデフォルトで有効になっています。このデフォルトで有効になっている profiles はそれぞれ違う意味があるので簡単に説明していきます。

まず、デフォルトで有効になっている profile は default profile です。これは上書きされていない限り leiningen/default profile が有効になるように設定されており、これは [:base :system :user :provided :dev] の合成として設定されています。つまり、通常であれば default profile というのは、 [:base :system :user :provided :dev] の profiles 全てが合成されている状態ということです。

dev profile はプロジェクト固有で利用する開発用ツールなどのために使います。ビルドやテストのときのみ必要となるものはこの profile に記述します。

user profile は :dev に似ていますが、 :dev がプロジェクト固有のものに利用されるのに対し、 :user はユーザーワイドなもののために利用します。衝突するのを避けるために、 user profile をプロジェクト固有の依存関係などのために利用しないでください(つまり project.clj やプロジェクトルート直下の profiles.clj には user profile を記述してはいけない)。その逆もしかりで、 dev profile をユーザーワイドな依存関係などのために利用することもできません。

system profile は :user と似ていて、違うのは任意のユーザーではなくシステムワイドに適用されることです。

base profile は基本的な REPL 機能のための依存関係を運んできます。 dev-resources:resource-paths に追加したり、デフォルトの :jvm-opts, :checkout-deps-share, test-selectors などを設定しますが、基本的にこの profile はユーザー側で変更すべきではありません。

また上記 4 つの profiles (dev, user, system, base)は開発中には有効になりますが、 jar ファイルや pom ファイルを作成する際には無効になるようになっているので、そのプロジェクトに依存するコードからは見えないようになります。

provided profile は jar を作成する際に利用可能で、プロジェクトに依存する他のコードには伝搬しない依存関係を指定するために利用できます。つまり provided profile で指定した依存関係は、そのプロジェクトが利用される環境側から提供されることを想定するもので、プロジェクトの開発中にも必要となるものです。具体的には Hadoop などのフレームワークで、幾つかのライブラリなどが提供されるような場合に利用されるみたいです。

よく Leiningen プラグインなどの利用方法を読んでいると ~/.lein/profiles.clj に以下のような記述をして利用してくださいとありますが、これはユーザーワイドな設定として user profile に記述してほしかったことが分かります。

{:user {:plugins [[jonase/eastwood "0.2.4"]]}}

profiles のマージ規則

profiles はプロジェクトマップ( project.clj )とプロファイルマップ( dev profile などのマップ)をそれぞれのキーについてマージしていきます。バリューがコレクションであれば結合しようとし、そうでなければ置き換えます。置換する場合、後ろに指定された profile が優先され(後述します)、 clojure.core/merge 関数のように動作するようになっています。デフォルトでは dev profile の方が user profile より優先されるようになっています。マップは再帰的にマージするようになっており、セットは clojure.set/union で統合され、リストやベクタは繋ぎ合わせられるようになっています。

後ろに指定された profile が優先されるというのは、 leininge/default profile の [:base :system :user :provided :dev] この指定の仕方を指しています。また先程のマージ規則はメタデータをヒントとして与えることによって、ユーザーが任意に変更することができます。優先順位を無視して利用してほしい値は :replace を使い、他の profile から与えられる値を優先したい場合は :displace を利用します。

{:profiles {:dev {:prep-tasks ^:replace ["clean" "compile"]
                  :aliases ^:displace {"launch" "run"}}}}

このような指定をすると :prep-tasks が他の profiles で指定されていたとしても、 dev profile が有効になっているとこれが採用されるようになり、 :aliases は dev profile 以外で指定されたもので置換されるようになります。

:plugins:dependencies だけは上述したマージ規則とは異なり、依存関係の重複排除を行なうロジックが組込まれます。ここでも同様に :replace, :displace が適用できます。

また profiles の宣言方法を紹介している中で上書きについて、触れましたが様々な場所にある同じ名前の profile は優先度の高いものがひとつだけ採用され、それぞれ同じ名前同士の profile をマージすることはしません。この優先度は次のようになっています。 profiles.clj > project.clj > ユーザーワイドな profiles > システムワイドな profiles 。これは profiles が何処のファイルで宣言されているかの違いであり、 user や dev などの profile の優先度ではないことに注意してください。

また、ここまでに説明したマージ規則を利用すると次のように記述することができることに気がつきます。

:profiles {:dev-common {:foo 1 :bar 2}
           :dev-override {}
           :dev [:dev-common :dev-override]}

このように記述した場合、 project 固有で全ての開発者共通の設定は dev-common profile に記述することができ、各開発者で上書きしたい設定は profiles.clj などの :dev-override に書くことできるようになります。

profiles を有効にする

デフォルトで有効になる profiles の話はしましたが、任意の profile を有効にする方法については説明をまだしていなかったため、ここで説明していきます。タスク毎に異なる profile を有効にしたり無効にしたりしたい場合は with-profile タスクを利用します。

まず Hiccup のような project.clj があると想定します。

(defproject hiccup "2.0.0-alpha1"
  :description "A fast library for rendering HTML in Clojure"
  :dependencies [[org.clojure/clojure "1.5.1"]]
  ;; 中略
  :aliases {"test-all" ["with-profile" "default:+1.6:+1.7:+1.8" "test"]}
  :profiles
  {:dev {:dependencies [[criterium "0.4.4"]]}
   :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}
   :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]}
   :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}})

この project.clj には 1.6, 1.7, 1.8 という profile がそれぞれ定義されており、依存関係に各 Clojure のバージョンを追加するものとなっています。

1.6 profile を有効にする場合:

$ lein with-profile 1.6 test

複数の profiles を組み合わせてタスクを実行する場合はカンマを挟む:

$ lein with-profiles dev,1.6 repl

複数の profiles それぞれでタスクを実行する場合はコロンを挟む:

$ lein with-profile 1.6:1.7 test

上の例はそれぞれデフォルトの代わりに任意の profile を有効にする方法でしたが、デフォルトに profile を加える場合は + を利用します:

$ lein with-profile +1.8 test

逆に - を使えば profile を無効化することができます。

応用: プラグインを利用したいときだけ有効にする

ここまでで書いてきたことを利用して、ちょっとした応用の話をします。

まずは昔話からはじめましょう。過去 CIDER ユーザーは Emacs で cider-jack-in をしているときはともかく、ターミナルでテストの実行(例えば lein test )をするだけでも Leiningen の起動が遅いと感じていました。 この原因は ~/.lein/profiles.clj の user profile にありました。

Emacs で CIDER を利用したい場合、以下のように ~/.lein/profiles.clj を書くことが求められていた時期があります。

{:user {:plugins [cider/cider-nrepl "0.14.0"]}}

これの何が問題なのでしょうか。

既にここまでで説明している通り Leiningen は通常幾つかの profiles をマージして起動します。デフォルトでは user profile もマージされる対象になっています。

この話の流れで察しの良い方は既に気付いていると思いますが、過去に CIDER ユーザーがターミナルでテストなどを実行する場合に感じた遅さというのは、 [cider/cider-nrepl "0.14.0"] という プログラムを書くときにしか利用しない 依存関係を user profile に含めてしまっていたのが原因だったわけです。

実際に簡単にテストして確認してみましょう。簡単なアプリケーションを lein new app demo として作成し、 time lein run を実行して速度を計測してみます。まずは ~/.lein/profiles.clj に何も記述していない状態です。

$ time lein run
Hello, World!
1.80user 0.12system 0:01.81elapsed 106%CPU (0avgtext+0avgdata 164376maxresident)k
0inputs+152outputs (0major+21200minor)pagefaults 0swaps

次に ~/.lein/profiles.clj を上述のように編集した場合です。

$ time lein run
Hello, World!
5.40user 0.24system 0:04.84elapsed 116%CPU (0avgtext+0avgdata 373300maxresident)k
0inputs+160outputs (0major+32266minor)pagefaults 0swaps

それなりに差があることが分かります。

さて、ここまで過去の CIDER ユーザーの問題として話を進めてきましたが、別に CIDER に限った話ではありません。 pprint, cljfmt, kibit, eastwood, ancient, …このように便利な Leiningen プラグインというのは沢山あるのですが、これを全て user profile 足していくとどうなるか言うまでもないでしょう。 これはどう解決するとよいでしょう。 もし 必要なときだけプラグインをプロファイルに追加 できるとしたら、そうしたいのではないでしょうか。

これは可能です。ということで以下に例を示します。

{:user {:aliases {"ancient" ["with-profile" "+plugins/ancient" "ancient"]}}
 :plugins/ancient {:plugins [[lein-ancient "0.6.10"]]}}

このように plugins/ancient profile というのを作っておいて、そこに ancient をプラグインとして追加する形です。 そして、 user profile に :aliases を定義しておくことで簡単に ancient を利用したい場合のみ、 ancient を profile に追加することができます。

ちなみに CIDER は Emacs 側で REPL を起動する際に自動的に cider-nrepl の依存関係を追加するようになっているので、現在では上記のような問題は起こらないようになっています。

小話: ライブラリの直接的な依存関係に Clojure や Ring は必要か

最近、 Clojure ライブラリを作成していて疑問に思ったことがあるのですが、 ライブラリを書く際に Clojure それ自身を project.clj:dependencies に直接書く必要はあるのでしょうか。

一般に Clojure ライブラリは利用される際に Clojure 自身が動作環境にあるはずです。とすると Clojure ライブラリを書く側は provided profile に Clojure の依存性を書くべきではないのでしょうか。

また Ring ミドルウェアも同様です。 Ring がない状況で Ring ミドルウェアを利用したいという状況は、ほとんどないはずなのでこれも Ring 自身を provided profile に依存性を書くべきでしょう。

Clojure はともかく Ring ミドルウェア系は最悪で、 Ring があることを前提にしているはずなのに、 Ring それ自身を直接的な依存ライブラリに含めているため、簡単に Ring のバージョンが衝突します(それ自身は Ring を依存関係に含めているとないのだけど、 Ring が依存しているものがバージョン違ったりして衝突する)。

まとめ

Leiningen の profiles は上手く使えれば便利です。