cabal sandbox環境のpackage DBを参照する

cabal-install-1.18 がきた.全国のHaskeller待望のcabal sandboxが使える.cabal-devさん今までありがとう.さようなら.


この記事は余計なことをしている可能性があります!


さて,テスト時にプログラムをビルドする必要があるとする.これは*.cabalファイルに設定したtest-suiteのプログラムのことではなく,test-suiteのプログラム内からさらにghcを呼び出してビルドするようなケースだ.たとえば,gracefulパッケージなどは「特定のsignalを受けて何かするプロセス」を作るためのパッケージなので,テストの中でそのプロセスをビルドして立ち上げている.他にもTemplateHaskell系のパッケージだとビルドそのものができるかというテスト書く必要とかもあるのでは?

この際,package DB(以前はpackage conf)が指定されていなければ,*.cabalに設定されたbuild-dependsが取れないので,通常は-package-db(もしくは-package-conf)オプションに"dist/package.conf.inplace"を渡す必要がある.

$ ghc --make Foo.hs -package-db "dist/package.conf.inplace"

しかし,cabal/cabal-devまでの世界であればconfig dist prefixは大体"dist"固定で考えていればよかった(debを作ろうとすると"dist-ghc"とか違う名前を設定したりするみたいだけど)が,cabal sandboxではそうもいかなくなった.sandbox環境ではconfig dist prefixが"dist/dist-sandbox-[HEX8桁]"という名前になる.このHEX8桁部分はどの設定ファイルにも吐き出されてないみたいなので,sandbox環境でも同様にテストを流すためには自分で構成してあげなければならない.

で,どうやらこのHEX8桁部分はJenkins hash functionで".cabal-sandbox"ディレクトリのフルパスをハッシュしたものらしい.たとえば,*.cabalファイルの置かれているディレクトリが"/home/notogawa/work/somepackage"で,この環境をsandbox化すると"/home/notogawa/work/somepackage/.cabal-sandbox"ができ,この環境では"/home/notogawa/work/somepackage/dist/dist-sandbox-975dcfa2"がconfig dist prefixになる.

結局,sandbox環境でもそうでなくとも適切にpackage DBを設定して*.cabalのbuild-dependsを反映させてビルドするためのコードはたとえば次のようになる.

packageOption :: String
#if __GLASGOW_HASKELL__ < 706
packageOption = "-package-conf"
#else
packageOption = "-package-db"
#endif

build :: FilePath -> IO ()
build file = do
  conf <- packageConf
  (code, _out, _err) <- readProcessWithExitCode "ghc"
                        [ "--make", file
                        , packageOption, conf
                        ] ""
  code `shouldBe` ExitSuccess

packageConf :: IO FilePath
packageConf = maybe "dist/package.conf.inplace" id `fmap`
              sandboxPackageConf

sandboxPackageConf :: IO (Maybe FilePath)
sandboxPackageConf = do
  cd <- getCurrentDirectory
  let prefix = cd ++ "/.cabal-sandbox"
  let confDistDir = "dist/dist-sandbox-" ++ showHex (jenkins prefix) ""
  let conf = confDistDir ++ "/package.conf.inplace"
  putStrLn conf
  exist <- doesFileExist conf
  return $ if exist then Just conf else Nothing

jenkins :: String -> Word32
jenkins str = loop_finish $ foldl' loop 0 str
  where
    loop :: Word32 -> Char -> Word32
    loop hash key_i' = hash'''
      where
        key_i   = toEnum . ord $ key_i'
        hash'   = hash + key_i
        hash''  = hash' + shiftL hash' 10
        hash''' = hash'' `xor` shiftR hash'' 6
    loop_finish :: Word32 -> Word32
    loop_finish hash = hash'''
      where
        hash'   = hash + shiftL hash 3
        hash''  = hash' `xor` shiftR hash' 11
        hash''' = hash'' + shiftL hash'' 15