これは ML Advent Calendar 2017 の 18日目の記事です (ずいぶん遅れました).
前回の記事 では Oasis について紹介しましたが,Jbuilder (Dune という名前に変わります) のほうがもっと楽だったので反省しつつ紹介します. 名前にセンスがないとか S式やだとか思ってしまいますが実際これは圧倒的に良いツールです. 今後は基本的には Jbuilder を使っていけばよい気がします(名前が変わるみたいですが…).
基本的には公式のマニュアルを見れば使えるのですが,public_name
や <package>.opam
についてはやや記述が散逸している感じなので適宜補足します.また,名前空間の衝突を避けるために Jbuilder が行う -pack
ライクな動作についても少し書きます.
よい点
- 自動的な依存解析,簡潔な設定ファイル.OASIS と違って、
.ml
を明示する必要さえない. -
Merlin (IDEのようなもの) の設定ファイル
.merlin
を自動生成してくれる. - 並列ビルド. Ocamlbuild は並列化されていなかったがこちらは
-j4
(デフォルト) が使える. OASIS + Ocamlbuild の謎のもっさり感がなくなるのが良いです.
よくない点
- S式. 大したハードルではないが,カッコの見た目や,構文的な面倒くささがある.例えば本来リストがあるべき箇所にリテラルを書いてしまうとエラーになるといったことが起こる (特にリストの要素が1つのときにやってしまう).
- PPX などのプリプロセッサが絡むと少し込み入ったカスタマイズが必要になる.
インストール
OPAM を入れて, jbuilder
と opam-installer
パッケージを入れる (opam-installer
については後述).
$ opam install jbulider opam-installer
使い方
- ソースコードがあるディレクトリ
jbuild
というファイルを置く -
jbulider build myapp.exe
を実行する. -
_build/default/myapp.exe
ができる.
…というのが基本的な流れです.ただ,アプリ名.opam
があれば jbuilder build
だけで済むので私はそのようにしています(後述).
実行形式 (.exe) を作る場合
(jbuild_version 1)
(executable
((name myapp)
(libraries (re lwt))))
を置き,ソース myapp.ml
を書く.
-
myapp.ml
が同じディレクトリの他のソースmysub.ml
,myutil.ml
, … を参照していても依存関係は自動的に解決される. -
libraries
は ocamlfind のライブラリのほか,他のディレクトリにあるライブラリも指定できる (後述). - 識別子は
"myapp"
のようにダブルクォートで囲んでもよい.
jbuilder を叩く.
$ jbuilder build myapp.exe
すると, _build/default/myapp.exe
が作られる. 楽だ!
これが例えば Haskell なら ghc --make
一発で済んでいたが、 OCaml もやっとここまで来たというわけだ。めでたい。
ライブラリを作る場合
ライブラリを作る場合, executable
を library
に変えるだけでよい.
(jbuild_version 1)
(library
((name mylib)
(libraries (re lwt))))
-
mylib.ml
があればそのモジュール (Mylib
) のみが公開される. -
mylib.ml
が無い場合は モジュールMylib
が自動生成され,他のモジュールは全てMylib
の中に入る(つまり-pack
される).例えば,modA.ml
,modB.ml
がjbuild
と同じディレクトリにあったとすれば,他のディレクトリのファイルからはMylib.ModA
,Mylib.ModB
という名前で参照できる.
ビルドは
$ jbuilder build mylib.cmxa
とすれば, _build/default/mylib.cmxa
ができる.
プロジェクトを複数ディレクトリに分ける
library
はプロジェクトを複数ディレクトリに分ける時にも使える.
app/ <-- 実行形式 myapp
jbuild
myapp.ml
lib/
jbuild <-- ライブラリ mylib
modA.ml
modB.ml
のようにして、 app/jbuild
の依存先として (libraries mylib)
を書けば,プロジェクトルート で jbuilder build app/myapp.exe
すれば mylib
を参照してくれる.
ワークスペース
-
jbuild-workspace
というファイルが上位のディレクトリに存在する場合、そこをルートにしてビルドが走る. -
jbuild-workspace
が存在する最上位のディレクトリがルートになる.jbuild-workspace
を含むリポジトリをclone
した場合でも、それよりさらに上位にjbuild-workspace
を置いておけば、そこがルートになる. - また,
jbuild-workspace.*
と拡張子を付与できる(が,使い所がよくわからない… 参考)
<package>.opam と public_name
プロジェクトのルートに <package>.opam
を置き、実行形式/ライブラリに public_name
を指定しておけば、jbuilder build
とするだけでビルドされるようになる(さらに, jbuilder install
や opam install
のインストール対象になる).
-
public_name
がない実行形式やライブラリはjbuilder build
のビルド対象にならない - ライブラリの
public_name
はパッケージ名と前方一致する必要がある.複数のライブラリをビルドしたい場合は,mylib
,mylib.ext
,mylib.sub
, .. のようにサフィクスを付ける.
例
myapp/jbulid
(jbuild_version 1)
(executable
((name myapp)
(public_name myapp) ; <<--
(libraries (re lwt))))
mylib/jbuild
(jbuild_version 1)
(library
((name mylib)
(public_name mylib)
(libraries (re lwt))))
mylib.opam
の内容は以下のような感じになる.以下,build
などは既存のものをそのまま使っている (詳しい書き方):
opam-version: "1.2"
maintainer: "keigo.imai@gmail.com"
authors: ["Keigo Imai"]
homepage: "..."
bug-reports: "..."
dev-repo: "..."
license: "Apache"
build:
[[ "jbuilder" "build" "--only-packages" "%{name}%" "--root" "." "-j" jobs "@install" ]]
depends: [
"jbuilder" {build}
"re"
"lwt"
]
これで jbuilder build
が効くようになるだけでなく,jbuilder install
でインストールできるようになるので配布がぐっと容易になります.
ワークスペースの使い方:依存先ライブラリをまとめてビルド
ワークスペースを有効利用すれば,必要なライブラリを一括してするのを助けてくれます.依存先のライブラリを OPAM でインストールするのではなく,同じディレクトリツリーに入れておけば,Merlin でソースコード間をジャンプできる利点があります.
例えば,cohttp
と ocaml-uri
が必要な場合,これらのライブラリは Jbuilder化されているので,
jbuilder-workspace
ocaml-cohttp/
ocaml-uri/
myproject/
myapp/
jbuild
...
mylib/
jbuild
...
mylib.opam
のようにして,
cd mylib; jbuilder build
すれば,ocaml-cohttp と ocaml-uri のビルドが走った後に mylib
がビルドされます.
余談: ライブラリの名前空間に関する詳細: -open と -no-alias-deps
Jbuilder は,名前空間を分離するため各モジュールの名前を微妙に変えてコンパイルします.jbuilder build --verbose
で追えば分かるのですが,これまでの OCaml コンパイラの動きとは少し違います.
具体的には,
-
-pack
のシミュレート.mylib.ml
が無ければ,module ModA = Mylib__ModA;; module ModB = Mylib__ModB;; ...
という中身のモジュールを生成する. -
mylib.ml
がある場合には-pack
相当の動作はない.しかし,これとは別にMylib__
という名前で上と同様のモジュールを生成する. - すべてのモジュールは,
ライブラリ名__モジュール名
にリネームしてコンパイルされる.例えば,modA.ml
はMylib__ModA
というモジュールになる.- 他のモジュールを参照するため,コンパイル時には上記の
Mylib
(ないしMylib__
) をopen
した状態でコンパイルする (オプション-open Mylib
か-open Mylib__
) - これは循環参照になるのでデフォルトではコンパイルできないが,
Mylib
ないしMylib__
のコンパイル時に -no-alias-deps を加えることで解決している.
- 他のモジュールを参照するため,コンパイル時には上記の
そのほかの話題
PPX を使ったり PPX を作ろうとすると少し工夫が必要になります。これまで幾つか雑多なツイートで書きましたが、今後、それについて記事にしていければと思います。