368

この記事は最終更新日から5年以上が経過しています。

投稿日

更新日

Haskellのビルドツール"stack"の紹介

Stackとは?

つい先日のことですが、Stackage界隈からstackというツールがリリースされました。リリースされたとはいえ、開発され始めたのがちょっと前のことですし、現在も盛んに機能が追加されているので、絶賛開発中であるとかそういったほうがいいかもしれません。

まだ開発の始まったばかりのツールなのに、なぜこんな紹介記事を書こうと思ったのかというと、このツールがHaskellの開発において極めて有用になることが確定的に明らかであって、すでに荒削りながらも、大変便利に使えているからなのです。そしてここで紹介することで、多くの読者の方に興味を持ってもらって、それで開発がさらに盛り上がっていくと嬉しいなあと、そう思った次第であります。

なお、stackの開発が始まる少し前に、stackage-cliを始めとするいくつかのツールがリリースされましたが、今後開発はstackに一本化されるようなので、これらのことは忘れましょう!

何をするものなのか?

Haskellのパッケージをビルドしたりインストールしたりするツールです。Haskellコミュニティーには、ビルドシステムとしてのCabalと、パッケージ管理ツールとしてのcabal-installがありましたが、stackはこのうちcabal-installを置き換えるものであって、パッケージングやバックエンドとしてはCabalを使っています。また、パッケージレポジトリとしてはHackageを使います。つまり、既存のインフラの上に、とてもうまく便利なツールを構築した形になっています。

何が嬉しいのか?

cabal-install を使っている人には、多かれ少なかれ不満を抱いている人が多いと思います。というか、十分満足している人のほうが少ないんじゃないかという気もします。stackはcabal-installのいろいろな問題を解決しています。

  • cabal hellに悩まされない

    Stackageを使ってパッケージの依存関係を解決するので、バージョンをあげたらパッケージ解決がコケるようになったとか、アレをいれたらコレが入らぬとか、再インストールでアレが壊れたとか、そういうことがなくなります。依存関係は基本的に一通りに解決され、再インストールも基本的にありません(詳しくは後述)。パッケージが壊れないし、バージョン違いを再インストールする必要もないので、cabal-installを使っている時のように、sandboxを毎回作って、その中で毎回一から全部依存ライブラリビルドして…、ということがなくなります。コンパイル時間が短縮されて、結構バカにならない容量を食うのサンドボックスも必要無くなります。

  • 複数バージョンのghc環境を簡単に共存させられる

    複数のghcのバージョンを共有させるには、インストールする場所を分けて、参照するパッケージDBの場所を正しく設定したり、あまり簡単とは言えない状況にありましたが、stackを使えばセットアップから使い分けまでとても簡単にできます。何も設定する必要はありません。

  • 複数のcabalパッケージからなるプロジェクトを簡単にビルドできる

    cabal-installでは最近実装されたsandboxの、add-sourceを使って複数パッケージからなるプロジェクトをビルドしていましたが、stackでは特に何も設定せずに簡単にこういうプロジェクトをセットアップできます。複数パッケージの設定も単純なyamlファイルの編集で変えられるので、とても簡単です。

Stackageとは

Stackageとは、HackageのパッケージのStableなセットを作るプロジェクトのようです(あまり詳しくはないのですが)。

ここでいうStableなセットというのは、

  • コンパイルが通る
  • テストが通る
  • 依存関係がセットの中で閉じている
  • セットに含まれるのは、ひとつのパッケージに対してただひとつのバージョン

という感じで、特にクオリティーなどへの要求ではなく、とにかくここに含まれているものなら、同時に何をどれだけ使おうともコンパイルが通るということを保証するというもので、これはパッケージ開発者にとって大変ありがたい性質です。

このようなStableなセットのうち、なるべく大きく、なるべく新しいものが含まれるようなものを維持し続けるのが(多分)Stackageであり、LTS Haskellなんじゃないかなあと思います(LTS Haskellというのは、Stackageのスナップショットのうち、二週間に一度作られるものです)。

もちろん大量のパッケージのあるHacakgeから自動的にこういうことをするには膨大な計算リソースが必要だし現実的ではないので、現状では何人かの人が手動でメンテナンスしているようです。これは大変な作業だと思うので、頭の下がる思いなのですが、こういうのがあってこそHaskellの商用利用も加速するといったものだと思うので、ありがたく使わせていただくことにします。

Stackage自身は、これはただのHacakgeのパッケージの集合のスナップショットなので、特別なツールを使わずとも利用することができます。Stackageのトップページにも載っているように、cabal sandboxがあるプロジェクトのディレクトリで、cabal.configをダウンロードするだけです。

$ wget https://www.stackage.org/lts/cabal.config
$ cabal update
$ cabal install

cabal.configには、バージョン付きパッケージの集合が列挙してあるだけです。

ではstackは何かというと、このStackageを用いてHaskellの開発を便利に行えるようにするツールです。Stackageでは、パッケージや依存しているパッケージのバージョンが固定されているので、本来はパッケージのコンパイルやインストールはただ一回だけで済むはずですが、実際にはcabalのsandboxで共有を行おうとすると、かなり面倒なことになります。また、Stacakge外のパッケージをうまく扱わないと、結局sandboxがおかしくなって、部分的にcabal hellを解決できていないことになります。

そこで、上に示したようなStackageのパッケージ集合としての性質を最大限に活かせるように設計されたのがstackです(多分)。これがあんまりにもうまく出来ていて、しっくりハマっているものですから、正直Stacakge初めて見た時は、こんなのそんなに役に立つんかな、とか思っていたんですが、今ではその構想の素晴らしさにただただ舌を巻いています。

と、そんなstackの使い方を見て行きましょう。

簡単な使い方

使い方はとても簡単なので、特にあんまり解説するべきこともないんですが、せっかくなので簡単なチュートリアルを書いてみようと思います。ちなみに、GitHubにおいてある動作イメージはこんな感じらしいです。

stack-build

stack.yamlを作る

プロジェクトのルートにstack.yamlが必要です。とはいえこれは自動で生成できます。

$ stack init

これによって、カレントディレクトリ以下に存在する全ての.cabalファイルがスキャンされて、それらを含むstack.yamlが生成されます。マッチする"resolver"が自動的に選択されます。

動作例を示してみます。

$ stack init
Writing default config file to: C:\Users\tanakh\Documents\GitHub\msgpack-haskell\stack.yaml
Basing on cabal files:
- C:\Users\tanakh\Documents\GitHub\msgpack-haskell\msgpack-rpc\msgpack-rpc.cabal
- C:\Users\tanakh\Documents\GitHub\msgpack-haskell\msgpack-idl-web\mpidl-web.cabal
- C:\Users\tanakh\Documents\GitHub\msgpack-haskell\msgpack-idl\msgpack-idl.cabal
- C:\Users\tanakh\Documents\GitHub\msgpack-haskell\msgpack-aeson\msgpack-aeson.cabal
- C:\Users\tanakh\Documents\GitHub\msgpack-haskell\msgpack\msgpack.cabal

Checking against build plan lts-2.15
Selected resolver: lts-2.15
Wrote project config to: C:\Users\tanakh\Documents\GitHub\msgpack-haskell\stack.yaml

ここではmsgpack-haskellディレクトリ以下の5つの.cabalファイルが走査されています。resolverとして、lts-2.15が選択されています。resolverとはなんぞやというのは次節で。

生成されたstack.yamlは次のような内容になっています。

stack.yaml
flags: {}
packages:
- msgpack-rpc/
- msgpack-idl-web/
- msgpack-idl/
- msgpack-aeson/
- msgpack/
extra-deps: []
resolver: lts-2.15

packagesの中に、ビルド対象のパッケージが列挙されます。resolverのところには、ビルドに使うresolverが入ります。

resolver

resolverとはなんぞやという話ですが、要するにStackageのsnapshotのことです。現在存在するのは

  • lts-1.x系列 オワコン
  • lts-2.x系列 現在の安定版。GHC-7.8.4向け
  • nightly 毎日のスナップショット。GHC-7.10でコンパイルするなら今のところこれを使わないといけない

この三つの系列です。まだ出ていないのですが、GHC-7.10向けの3.x系列もそのうちリリースされるようです。デフォルトではlts-2系の新しいものから順に適合するかチェックされていきます。--resolverオプションを指定することで、特定のresolverを強制することができます。

$ stack init --resolver=nightly-2015-06-24

stack.yamlを編集する

また、stackではStackageに含まれていないパッケージも完全にうまく扱えるような機能が用意されています。それどころか、Hackageにアップされてないものも依存関係に取り込めるようになっています。

stack.yamlのフォーマットを詳しく解説していたらすごい量になってしまいますし、将来的に陳腐化する気もしますので、https://github.com/commercialhaskell/stack/wiki/stack.yamlこちらを参照していただくとして、ここではめぼしいものを解説したいと思います。

extra-deps:には、resolverに含まれない、あるいは含まれているけど別のバージョンを使いたいときなどに、それを明示的に指定できます。たとえば、stack自身のstack.yamlでは次のようになっています。

stack.yaml
packages:
- .
extra-deps:
- optparse-simple-0.0.3
- path-0.5.1
- monad-unlift-0.1.1.0
resolver: lts-2.9

packages:のところには、プロジェクト内のパッケージを列挙するのでした。

stack.yaml
packages:
- dir1
- dir2
- dir3

ここにはディレクトリだけではなく、http上のtarballや、gitのレポジトリを書くこともできます。

stack.yaml
packages:
- some-directory
- https://example.com/foo/bar/baz-0.0.2.tar.gz
- location:
    git: git@github.com:commercialhaskell/stack
    commit: 6a86ee32e5b869a877151f74064572225e1a0398

gitのレポジトリは、特定のコミットを参照する必要があるようです。これは多分時間とともに違うファイル内容を指すようになるのは、stackとして望ましい挙動ではないからでしょう。

この機能によって、未リリースのパッケージに依存するパッケージを開発したり、既存のパッケージにパッチを当てたものに依存するパッケージを開発したりするようなことが、とても簡単にできるようになっています。

その他、地味に嬉しい機能として、

stack.yaml
extra-include-dirs:
- /opt/foo/include
extra-lib-dirs:
- /opt/foo/lib

extra-include-dirsextra-lib-dirsを指定する機能があるようです。これまではcabal configureの時に手で毎回与える必要があったので、これが設定ファイルにかけるようになるとずいぶんはかどりそうです。

プロジェクトをビルドする

stack.yamlができたら、あとはstack buildを実行するだけです。

$ stack build

これだけで、ビルドに必要なことを全部やってくれます。依存ライブラリのインストールから、happy、alexなどのビルドに必要になるツール、はてはghc自体までダウンロードしてインストールしてくれます。Windowsでも、Linuxでも、適切なやつが落ちてきて、適切な場所にインストールされます。そしてこれらは、全て特別なディレクトリに置かれるので、システムが壊れる心配をする必要はありません。

依存するパッケージがresolverの中に見つからなかったとき、stackはcabalにフォールバックしてHackageのパッケージ全体を探します。

例えば、このように、

$ stack build
While constructing the BuildPlan the following exceptions were encountered:

--  Failure when adding dependencies:
      optparse-declarative: needed (>=0.3), but not present in build plan, latest is 0.2.0
    needed for package: hoe-1.1.0

--  While attempting to add dependency,
    Could not find package optparse-declarative in known packages

Recommended action: try adding the following to your extra-deps in C:\Users\tanakh\Dropbox\project\hoe\stack.yaml
- optparse-declarative-0.2.0

You may also want to try the 'stack solver' command

どのパッケージを解決するのに失敗したのか、なぜ失敗したのか、そして我々はどうするべきかまでを、懇切丁寧に教えてくれます。stack.yamlのextra-depsへ何を追加したらいいのかがyamlのフォーマットで出力されるので、大体はこれをコピペして貼り付ければビルドが通るようになります。便利すぎる。

また、stack buildのオプションとして--resolver=xxxを与えると、stack.yamlで指定したresolverの設定を上書きすることができるので、例えば、普段はresolverにnightly-xxxx-xx-xxなどを指定しておいてghc-7.10で開発し、リリースするときにstack build --resolver=2.15としてやることでghc-7.8.4でもコンパイルできるかを簡単に試すことが出来ます。

プログラムを実行する

ビルドできたプログラムは、

$ stack exec <プログラムの名前>

で実行できます。ライブラリプロジェクトの場合は、

$ stack ghci

とすることによって、プロジェクトのモジュールを参照できる状態でghciが起動します。

$ stack runghc <haskellファイル>

とやれば同様にrunghcで実行できます。

$ stack test

でテストスイートを実行、

$ stack bench

でベンチマークを実行できます。

プログラム/ライブラリをインストールする

$ stack install <パッケージ名>

とやると、cabal installの要領で、パッケージがインストールされます。インストールされたパッケージは、resolverに含まれるものならグローバルなところにインストールされ、そうでないものは、プロジェクトローカルなところにインストールされます(これはresolverに含まれないものがグローバルにインストールされると、バージョンを固定できなくなって、あとでcabal hellみたいなことになるからだと思います)。実行ファイルは~/.local/binにインストールされます。そういう仕組ゆえに、stack installはどこで行ってもあまり違いがなく、cabal sandboxを使っている時のように、ここはsandboxのあった場所だろうか、などと常に意識する必要がなく、かなり楽になります。

パッケージをアップロードする

$ stack upload

cabal uploadと同じことをするものですが、cabal uploadと違って、

  • tarballを自動的に作ってくれるので事前にcabal sdistでアーカイブを作ってたりファイルを指定する必要がない
  • httpsによって安全な経路でアップロードしてくれる
  • ユーザー名とパスワードを二回目以降覚えていてくれる

という実に痒いところに手が届く仕様になっています。

その他のコマンド

stack new

プロジェクトのひな形を作ってくれる。まだあんまり大したものは出てこないけど、それなりには便利。多分将来的にはずいぶん便利になる。

stack setup

指定したバージョンのghc環境をインストールしてくれる。とはいえ、これはresolverに紐付いたghcのバージョンが入ってなければ、stack buildが自動的にインストールしてくれるので、明示的にやる機会はあんまりないかもしれない。

stack update

パッケージのインデックスを更新する、cabal updateに対応するコマンドですが、resolverはパッケージのバージョンを固定するものですし、extra-depsもバージョンを完全に固定して使いますので、明示的に必要になるケースは殆ど無いと思います。ライブラリの解決ができなかったとき、自動的に最新のHackageのインデックスを見に行く仕様になっているので、そのへんも抜かりはなさそうです。

stack docker

dockerを使ってないのでわからないのですが、stackのdockerイメージが有るみたいで、それを必要に応じてとってきてくれたり、その中でビルド作業してくれたりするらしいです。今度使ってみます。

まとめ

というわけで、stackの紹介でした。説明してきたように、すでにずいぶん便利なツールなのですが、これからさらに便利になっていくと思いますので、どしどし使ってフィードバックを送りましょう。

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について
368