OCaml
Dune

Jbuilder (Dune) でもっと楽に OCaml プロジェクトを作る

これは 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 を入れて, jbuilderopam-installer パッケージを入れる (opam-installer については後述).

$ opam install jbulider opam-installer

使い方

  1. ソースコードがあるディレクトリ jbuild というファイルを置く
  2. jbulider build myapp.exe を実行する.
  3. _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 もやっとここまで来たというわけだ。めでたい。

ライブラリを作る場合

ライブラリを作る場合, executablelibrary に変えるだけでよい.

(jbuild_version 1)

(library
 ((name mylib)
  (libraries (re lwt))))
  • mylib.ml があればそのモジュール (Mylib) のみが公開される.
  • mylib.ml が無い場合は モジュール Mylib が自動生成され,他のモジュールは全て Mylib の中に入る(つまり -pack される).例えば, modA.ml, modB.mljbuild と同じディレクトリにあったとすれば,他のディレクトリのファイルからは 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 installopam 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 でソースコード間をジャンプできる利点があります.

例えば,cohttpocaml-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.mlMylib__ModA というモジュールになる.
    • 他のモジュールを参照するため,コンパイル時には上記の Mylib (ないし Mylib__) を open した状態でコンパイルする (オプション -open Mylib-open Mylib__)
    • これは循環参照になるのでデフォルトではコンパイルできないが,Mylib ないし Mylib__ のコンパイル時に -no-alias-deps を加えることで解決している.

そのほかの話題

PPX を使ったり PPX を作ろうとすると少し工夫が必要になります。これまで幾つか雑多なツイートで書きましたが、今後、それについて記事にしていければと思います。