yunomuのブログ

酒とゲームと上から目線

cabalでビルドをカスタマイズする

 cabalファイルだけでは足りない事がある。どうしても単一のプログラムではできないこともあるし、TemplateHaskellでも生成できなくてやむなくソースコードを自動生成するスクリプトを書かざるを得ない事もあります。自動生成なんかやると何かに負けた気分になりますが、cabalでなければ普通にやっている事なので、何も問題は無い気がします。MakefileとかAntとか。

 具体的には、
(1) Haskell製コード生成プログラムをビルド
(2) シェルスクリプト経由でコード生成プログラムを実行
(3) 本体のプログラムをビルド
という事をやりたい。

 ところがcabalには直接的に外部のコマンドを実行してビルドする方法が無いみたいです。無いみたいなんですが、ビルドを細かく制御する方法はいくつか用意されています。
(A) Build-Type: Configureでautoconfで頑張る
(B) Build-Type: MakeでMakefileを書いて頑張る
(C) Build-Type: CustomでSetup.hsを書いて頑張る

 はっきり言って(A)で何でもできるんですが、それなりに面倒くさい。(B)は楽そうだけど、全部のターゲット(http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Make.html)を定義するのは結構面倒だし、そんなに色々出来る必要は無い。cabalの機能もそれなりに使いたいし。どうでもいいけど、Makeのときにcabal configureの時にconfigureファイルが無いとコケるのはバグかな。空のconfigureファイルがあればその後は問題なく通っちゃうし、configureスクリプト使ってないっぽいのでバグっぽい。誰も使ってないんだろうか。まあ(A)と(C)があれば使わないよな。

 そんなわけで、余程の事がなければCustomで足りると思うのでCustomを使って行こうと思います。

 ここで、こんな感じのプロジェクトを作ったと仮定します。この例はHaskellのCabalのマニュアルからのコピペ(http://www.haskell.org/ghc/docs/7.0.3/html/Cabal/authors.html)のBuild-TypeをCustomに変えただけのものです。

Name:           TestPackage
Version:        0.0
Cabal-Version:  >= 1.2
License:        BSD3
Author:         Angela Author
Synopsis:       Small package with two programs
Build-Type:     Custom

Executable program1
  Build-Depends:  HUnit
  Main-Is:        Main.hs
  Hs-Source-Dirs: prog1

Executable program2
  Main-Is:        Main.hs
  Build-Depends:  HUnit
  Hs-Source-Dirs: prog2
  Other-Modules:  Utils

 今回はこれを、program1をビルドするためにprogram2とシェルスクリプト(build.sh)が必要だということにしよう。

 これに加えて、cabal initした時に作られるSetup.hsがあります。
 こんなの。

import Distribution.Simple
main = defaultMain

 このままの場合は、Build-Type: Simpleの時と全く同じ動きをします。
 ちなみにimportの部分をDistribution.Makeに変えるとBuild-Type: Makeと同じ動きになります。

 つまり、cabalコマンドを実行した時にこのmainから始まるプログラムが実行されるわけなので、例えば

main = return ()

などとやると何をしても何も起こらなくなります。いや、helpは大丈夫ですけど。

 じゃあconfigureとかbuildとかtestの判別はどうやってるんだというと、単にdefaultMainの中でgetArgsしているだけなので、

import System.Environment
main = getArgs >>= print

なんてやってみると、このSetup.hsというものがどれだけ何でもアリなのかがわかるんじゃないかな。ただ残念ながらcabalに定義されてないサブコマンドを作る事まではできません。

 というわけで、getArgsの中身を見ながら処理を書いてもいいんですが、それだとMakefileと変わりません。ここではcabalが知り得る色々な情報を用意してくれるCabalパッケージのライブラリを使って処理を書くことができます。
Cabal: A framework for packaging Haskell software | Hackage

 具体的には、例えばbuildサブコマンドの前に処理を挟みたい場合はこう

import Distribution.PackageDescription (HookedBuildInfo)
import Distribution.Simple
import Distribution.Simple.Setup (BuildFlags)

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{preBuildHook = hook}

hook :: Args -> BuildFlags -> IO HookedBuildInfo
hook args flags = ...

何ができるかは、おおむねUserHooks型のマニュアルを見ればわかるかと思います。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple.html#t:UserHooks

 これの中の、例えばbuildHookという、これはhookというよりはbuildコマンドを実行した時に行われる処理そのものなんですけど、そのbuildHook関数の定義がこうなってる。

buildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()

この中で、

  • PackageDescriptionは*.cabalファイルの中の全体に関わるNameとかDescriptionの部分
  • LocalBuildInfoはExecutable節とかLibrary節みたいな部分
  • UserHooksは定義したUserHooksそれ自身
  • BuildFlagsはサブコマンドに渡したターゲットやオプション

になっています。
 まるっきり、なんの工夫もなく、cabalの実行時に得られる情報そのものです。
 そのものなので、例えばbuildコマンド実行時にinstallを実行したりもできます。

import Distribution.PackageDescription (HookedBuildInfo)
import Distribution.Simple
import Distribution.Simple.Setup (BuildFlags, defaultInstallFlags)

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{buildHook = hook}

hook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
hook desc info hooks _flags = (instHook hooks) desc info hooks defaultInstallFlags

これでbuildだけを実行することはできなくなり、buildするとinstallを必ず実行するようになりますが、installコマンドを実行すると無限ループするので使い物にはなりません。

 ここで最初の例に戻ります。
 program1をビルドするためには(1)program2をビルドして(2)build.shを実行した後に(3)program1をビルドする必要があります。
 まあ、これだけの情報があればこれくらいの処理は簡単に作れそうに見えます。

 ところが、これが意外に面倒くさい。
 hooksに渡される情報はあくまでもコマンドに渡される引数とcabalファイルの情報だけなので、これからビルドするターゲットがどれかという情報は含まれていない。program1をビルドしようとしているのかprogram2をビルドしようとしているのかの区別は存在しないのです。
 なので、(3)program1をビルドする前に(1)program2をビルドして(2)build.shを実行する、みたいな順序付けは非常に面倒くさいのです。順序付けならcabalのBuild-Tools: program2でできますけど、プログラムの実行ができない。

 ただ、build処理内でprogram1とprogram2の区別はされていないものの、一応何を実行しようとしているかは知ることができます、BuildFlagsのbuildArgsで。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple-Setup.html#t:BuildFlags
 このフィールドには、buildサブコマンドに渡されたターゲットが格納されています。

% cabal build program1

を実行すれば["program1"]が、

% cabal build program1 program2

を実行すれば["program1", "program2"]が、

% cabal build

を実行すれば[]が入ってきます。
 お察しの通り最後のが厄介で、buildはターゲットを指定しないと全てのターゲットをビルドするんですが、その時は空リストが格納されています。でも落ち着いてPackageDescriptionのexecutablesから全ターゲットのリストが取れるので、それを使えばいいと思います。こんな感じで。

import Distribution.PackageDescription (HookedBuildInfo, executables, exeName)
import Distribution.Simple

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{buildHook = \desc _ _ _ ->
    print (map exeName (executables desc))}

 あとは、buildArgsの中身を見ながら、program1がビルドされるかどうかを判断してprogram2をビルドしたり、System.Processモジュールを使って外部コマンドを実行したりすればいいんじゃないかと思います。最後は力技です。面倒くさいです。

 さらに面倒なことに、これら面倒な事をパッケージ化してライブラリとして公開したとしても、cabalにはSetup.hsをビルドするための依存ライブラリを記述する機能が無いので、そのライブラリが既にglobalかsandboxにインストールされていないと本体(program1とか)のビルドがコケる。
 これなんとかならないのかな。もはやCabalに機能追加するしか無い気がする。
 いやtargetのどれかのBuild-Dependsに書けば大丈夫なんですけど、targetに関係無い依存関係書きたくないよねぇ。私が使っているのは今のところprocessとdirectoryで、これらは本体でも使っているので問題は起きていませんけど。悩ましい。