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

前回の記事は調査不十分だった.

まず,sandbox環境では,

cabal install --only-dependencies --enable-tests
cabal configure --enable-tests
cabal build
cabal test

を実行したときと,

cabal install --enable-tests

を実行したときで,config dist prefixが異なる.前者では,これまで同様にdistで,後者ではdist/dist-sandbox-HASHとなる.HASHの決まり方は前回の記事参照.

このHASH付きのprefixが使われるのはちゃんとした理由がある.cabal sandboxにはadd-sourceというローカルパッケージディレクトリに依存させる機能がある.どういうことかというとパッケージBがパッケージAに依存していて共にローカルにソースツリーがある場合.

cd /path/to/B
cabal sandbox init
cabal sandbox add-source /path/to/A
cabal install --only-dependencies

とすることで,ローカルに置いてあるパッケージAを依存パッケージとして使うことができるというものだ.このとき,BのためにビルドされるAは./path/to/A/dist/dist-sandbox-[Bの.cabal-sandboxのフルパスを基準にしたHASH]以下で作業される.もし,パッケージAをパッケージBとパッケージCからそれぞれcabal sandbox add-sourceするような場合に,Bからcabal installする場合のAの依存関係はBの依存関係に引きずられ,Cからcabal installする場合のAの依存関係はCの依存関係に引きずられるため,それぞれ違うパッケージバージョンセットに依存してビルドされたAが必要になっているかもしれない.同じdistディレクトリを使っていたままでは,BとCを並列にビルドするようなときに衝突してしまうため,Bを基準に決まるHASH,Cを基準に決まるHASHの付いたディレクトリにそれぞれ分けられてA以下でビルドされる.

このことがテストなどビルドが必要な操作に影響するのは前回書いた通りであるが,前回はcabal install --enable-testsのときの話しかしておらず,sandboxかどうかだけで分ければいいと思っていた.明らかに間違いである.正しい認識では「sandbox環境ではどのcabal subcommandからテストが実行されるかによってconfig dist prefixを変えなければならない」ということになる.sandboxかどうかだけでは判断できない.

これに影響を受けるもので最も身近なのは恐らくdoctestだと思う.CPP拡張やPath_*.hsを使っているモジュールに対してdoctestを実行するような場合などは必ずautogen以下を使う必要がある.たとえば,test-suite doctestsのコードは,

-- doctests.hs
import Test.DocTest

main :: IO ()
main = doctest [ "-isrc"
               , "-idist/build/autogen/"
               , "-optP-include"
               , "-optPdist/build/autogen/cabal_macros.h"
               , "Test.Target.Module"
               ]

といった具合になるわけだが,このままではこの記事の冒頭に示した2通りのテスト実行コマンド列をカバーできておらず,実際cabal install --enable-testsのほうであたりまえのようにコケる.cabal_macros.hがみつからないか,みつかったとしても期待してるのと別のファイルを見てるハズだ.なので,2通りのどちらで実行されたかを知らねばならないのだが,残念なことにあまり良い方法が無い.Setup.hsに手を入れて上手いこと判断できるような気はするのだが,Setup.hsを弄るのは面倒が増えるし地獄っぽいのであまりやりたくない.というかCabalパッケージをあまり見たくない.目がー!ってなる

結局,テスト実行バイナリが自身のファイルの位置を基準にする方法がリーズナブルではないだろうか.バイナリファイルの位置は,前者のケースではdist/build/doctests/doctests,後者のケースではdist/dist-sandbox-HASH/doctests/doctestsになる.たとえば,linux環境下では/proc/self/exeがプロセス自身のファイルへのシンボリックリンクになるので,test-suite doctestとしては,

-- doctests.hs
import Test.DocTest

import System.FilePath
import System.Posix.Files

getConfDistDir :: IO FilePath
getConfDistDir = fmap (dirname . dirname . dirname) getModuleFile where
    dirname = takeDirectory
    getModuleFile = readSymbolicLink "/proc/self/exe" -- ghc 7.6.1 以降なら getExecutablePath

main :: IO ()
main = do
  confDistDir <- getConfDistDir
  doctest [ "-isrc"
          , "-i" ++ confDistDir ++ "/build/autogen/"
          , "-optP-include"
          , "-optP" ++ confDistDir ++ "/build/autogen/cabal_macros.h"
          , "Test.Target.Module"
          ]

のようになる.これなら冒頭に示したどちらのケースで実行されても期待する通りにテストが走る.Windowsは知らないけどたぶん似たようなことできるだろう.ghc 7.6.1 以降なら getExecutablePath でよさげ.でも,現在のdebian wheezyはghc 7.4.1なので,そこへんまでカバーさせたいライブラリでテストも対応させたいなら別の方法で.

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

ICFP Programming Contest 2013

今年ものとがわさんパンピーやった(1年ぶりn度目)


問題はここ.64bit非負整数に対する限られた関数と単純なlambda式からなるプログラムがコンテストサーバ側にいっぱいあってIDが付いている.サーバ側にある各プログラムの具体的な形はわからない.プログラムは全て 64bit非負整数->64bit非負整数 の型を持つ.サーバ側には

  • myprograms: プログラムのサイズや含まれてる関数などの一覧を取得するAPI
  • eval: あるIDのプログラムに,指定した入力を適用したときにの出力を一度に最大で256組取得するAPI
  • guess: あるIDのプログラムはコレじゃないの?と回答するAPI

などが用意されており,解きたい問題をmyprogramsから選んでevalでブラックボックスに入出力ペアを取得し,プログラムを推定して最初のevalから5分以内にguessする.正しければ1点ゲット.

  • train: 練習用のプログラムが得られるAPI

もあり,一応テストできるが,trainも含めAPIには1チームあたり20秒あたり5回までというアクセス回数制限がかかっている.

割と誰でも手をつけやすい敷居の低さで,しかも難しいものは難しく,それに対処したときの効果もわかりやすい.大きな難易度幅を持ち,参加者の思考を促すようなとても良い出題だったと思う.APIのアクセス回数制限によって,時間を消費させつつもできるだけ早くアイデアを形にしなければならないという煽りも秀逸.ただ多くの参加者が思うようにleaderboardが無い理由だけはよくわからなかった.

言語はHaskell

結果はこんな感じ.

リポジトリここで,コンテスト中に書いた分はだいたい以下の通り.

   53   235  2038 ./BV-proxy/main.hs
   49   286  1668 ./language-BV/Language/BV/Eval.hs
  155   967  5760 ./language-BV/Language/BV/Syntax.hs
  627  3510 26420 ./BV-infer/main.hs
  884  4998 35886 合計

1日目

問題を読んで,うーんこの.と思いながらとりあえずASTと評価器を作る.Haskell.特に問題無し.プログラムにはfoldがいっこしかないとかなので,"foldを含むかどうか",と"fold内にある式なのかfoldの外にある式(もしくはfold)なのか"をGADTsでタグ付けしたりTypeFamiliesとか使ってたりして型で区別しておく.これでプログラムにfoldを2つ埋めてしまうとかイロイロとミスできなくしておく.Eqとか後述のArbitraryのインスタンスにするとかが困る感じになったがまぁそのへんは雑に.

次に肝心のプログラム推定器だが,じゃあまず「運ゲー状況作るか!」といきなり思考停止.画面見ない勢の本領発揮.

おもむろにASTをTest.QuickCheck.Arbitraryクラスのインスタンスにして同じくTest.QuickCheck.sample'でランダムサンプリングしたプログラムがたまたまevalの結果と正しいかどうかをチェックしてguessするようなものを書いた.あるサイズ以下のプログラムをランダムに生成させたかったのだが,Arbitraryは型からしか情報が取れないので,手っ取り早くunsafePerformIOでIORefに「今解いてる問題の情報」を持ってしまうことにした.どうせアクセス回数制限と5分時間制限のせいで,複数の問題を同時に処理するつもりは無くなってたため,このunsafeはまぁそんなにunsafeじゃないだろうと.これで小さいプログラムに対しては大体解けるってくらいの運ゲー感.2日目3日目でコイツが大きなサイズをたまたま解いてる!?のを見るとかなり得した気分になる程度.

全体としてはシンプルでソルバを起動すると,

  1. myproblemsで問題を全部取得
  2. 解けそうなやつを選ぶ
  3. evalで人口的に適度にビット立てた256入力に対してサンプリング
    • ビット演算が主だったのでビットの位置関係に配慮した
  4. この256サンプルを満たすものを求める
  5. guess

で最後までこれはほぼ変わらない.毎回myproblemsするのも効率悪いけどマシン複数台使ったりとかそんなリソース無いし,これでもザックリ1時間で最速300問処理れるからまぁいいかなと.ゆっくりしていってね

とりあえずコレをmyprogramsの小さい問題やtfoldの問題を選んで回してlightning用とする.まだこの時点でアクセス制限対策は無く,1問トライしたら20秒待つという雑な仕事して夜は寝る.ただ変に回っちゃって易い問題を失敗で消費してしまうのも怖いので何かエラー想定外状況時はそこで見切りを付けて終わり.案の定起きたらmissmatchで止まってた.

2日目

さすがにちょっと運ゲーだけだとダメだよなーと少し真面目になる.反省

guessの結果がmissmatchだったらその反例も加えて再度ブン回す処理を追加.

あるサイズのプログラムを全列挙し初める.ただし探索するときは問題に与えられたサイズ以下のプログラムを小さいほうから探索する.1日目に運ゲーでtrainやmyproblemsを解いてるとき,

  • 問題のプログラムには無駄な演算が入っていることが多々ある
    • サイズや演算が必要最小限よりも大きく多くなっていることが多々ある
    • 2日目終わって寝てる間にヒントとして公式にアナウンスされた
  • 別にサイズや演算が同じでなくても同じ結果を与えればスコアになる

ことは気付いていたので,小さいプログラムで済むならそれが速い.演算の性質も鑑みて適宜枝刈りも入れていった.結局この全探索と運ゲーに加え,5分のタイムアウトをControl.Concurrent.Asuncで並列化.

パワーが足りなくなってきたのでVMへのリソース割り当てを増やす.メモリ8GB,8コアからメモリ32GB,12コアへ

また,20秒待つとか雑なコントロールを廃止し,アクセス制限対策のコントローラを書いて推定器とコンテストサーバの間に入れた,と言っても普通のサーバに20秒(安全のため+数秒してるが)で復活するリソースを5個持たせ,それがひとつ取得できるまでは接続されても何もせず待ち,取得できたらプログラム推定器からの通信をコンテストサーバに横流しし,コンテストサーバからの通信内容を推定器に横流しするだけ.STMモナド無い言語でこういうの書く気もうまるで起きないのですが皆様いかがお過ごしでしょうか?

おもむろにこのエントリを書きはじめる.

イロイロ微調整したりしてると夜になったので走らせて寝る.起きたらサーバから "Unable to decide equality" 出てきて止まってた.なんだこれ.

3日目

3日目開始あたりで11位から50位がスコアレンジ300-550にいるとのアナウンスが.あれ?案外点数低そう?上はもう1000余裕みたいな感じじゃないの?とか思ってた.

bonusとかいうのが出てていかにも重そうだったのでひとまず無視するコードを追加.

最終日だし翌日昼間の仕事もあるので休暇をとらねばと,何かエラー出ても無視するようにしてひたすらブン回しておき,自分はニコ動を見たり暑い中蛍光灯を買いに出たりといった功夫を積んで過ごす.マスターオブライフ

そろそろbonus以外の問題ひととおりトライし切るかなと思ったあたりでbonusについて考え初める.bonusの特徴は(lambda (x) (if0 P T F))に決まってるっぽかったので,まずは1問に対するevalの回数を増やし,決め打ちの256+ランダム256*3の1024サンプル取得するようにした.まずはT(もしくはF)の候補Eをサイズ1から生成していって(lambda (x) E)でサンプルを処理してみる.先頭がif0ということは,あるプログラム(lambda (x) E)が1024サンプルの半分以上をパスするのであれば,このEはT(もしくはF)の候補に価すると判断する.今度はパスしなかったサンプルだけかき集めて,これらを全て通すようなF(もしくはT)候補を求める,あとは(if0 E T F)か(if0 E F T)となるようなEがあればこのEをPとみてguessを投げた.これは30未満のbonusをサクサク解いていってくれた.さすがに31以上はキツいのかタイムアウト多い.

夜になったのでこれをブン回して寝る.朝起きて出社前にプログラム止めて最終スコア確認.bonusはbonusだけあってそれなりにおいしかったようだ.

やったことメモ

  • foldの含有,fold内外をGADTで型制約
    • ArbitraryとGADTスゲー相性悪い感があるけど自分だけですかね?
    • あとEqにも困った
    • 上手く使えてない感,反省
  • アクセス数制御対策のproxy
    • 作成中のサーバ用ライブラリがあったので,50行くらいで
  • プログラム中の変数はx,y,zだけ
    • fold が無い場合,式中の自由変数は高々1個(x)
      • プログラム先頭のlambdaが束縛する分
    • fold が有る場合,fold外の自由変数は高々1個(x)
      • プログラム先頭のlambdaが束縛する分
    • fold が有る場合,fold内のlambda内の自由変数は高々2個(y,z)?
      • プログラム先頭のlambdaが束縛する分(x)は入ってこない?
      • この制約は記述がたぶん無かったがtrainをいくら引いてきてもそうだったので決め打ち
    • tfold には shadowing があるので2個(x,y)
  • プログラム生成順序
    • 解こうとしてる問題サイズより小さいものから生成
    • fold > if0 > op2 > op1 の順に先頭への出現優先度 (気分)
    • 二項演算と単項演算については,できるだけ適用先でなく演算のほうが入れ変わるように (気分)
  • プログラム生成時の枝刈り
    • not . not
    • (and/or/xor) E E
    • (not以外のop1) 0
    • op2 0
    • if0 (0/1) T F
    • if0 P E E
    • shr*が適用され続けるときはshr1→shr4→shr16の適用順に正規化

やれてなかったことメモ

  • プログラム生成時の枝刈り
    • op2 a b と op2 b a を同一視する
      • なぜやってないことに気付いていなかったのか…
    • size nのプログラムを作るときにsize 1のもの(0/1/x/y/z)を含んでいた…
      • bonusやってるときに気付いて刈った
      • bonus以外は刈ってないものでやってしまった
      • どういうことなの?これものすごい計算無駄にしたのでは…

やらなかったアイデアとか

  • サイズ大きいプログラムは予めデータベース化しておく
    • 以下の情報を持たせてクエリできるようにしておく
      • サイズ
      • 含まれる演算
      • 決まった定数個のサンプルに何を返すか
    • 枝刈りwith全生成の質にもっと早く納得できればやってみたかも
  • モンテカルロ木探索的な生成
    • "有力な枝"を決める基準であまりよいものができなさそうだと感じた
  • GAやSA
    • "良いやつを少し変更したものも同じくらい良い"ような気があまりしなかった
    • 5分で辿り付けるかが疑問だった

anarchy proofのAgda版作った

全国一億三千万のAgdarの皆様に送る Agda Challenge

最近chaton haskellがchaton agdaと化していたりすることもあり,anarchy proofがうらやましくてAgda版が欲しい人いっぱいいるのではないかと思って作ってみた.開発中にステージング環境を作り近くの数人にアナウンスしたけど何故か誰も試してみてくれないという悲しい事態を経たりしたのでもう公開する.きっと恥ずかしがりやさん達だったのだろう.

個人的にyesodとその周辺に関する知識をアップデートする目的があってyesodを採用している.デザインはあまり弄る気が起きずほぼ素のyesod 1.2のまま.あなごる・あなぷるでは採用してないが,認証系まわりも使ってみるためにOAuth認証を付けている.現在Twitterのみ.

使い方は大体あなぷると同じというかトップページの文面からして殆ど丸パクである.kikさんごめんなさい.

問題を作るときはDefinitions.agda(必要なら),Theorem.agda,Verifier.agdaを作って投稿する.Theoremは(あれば)Definitionsをimportし,証明すべきものをpostulateしておく.Verifierは(あれば)DefinitionsとTheoremをimportし,Theoremでpostulateしたものを使った検証用コードを書いておく.作成時はいずれも--safeオプション無しで検証される.

問題を投稿するときは(あれば)Definitions.agdaとTheorem.agdaをダウンロードし,Theorem.agda内のpostulateを消して証明したものを投稿する.投稿時はDefinitionsだけ--safeオプション無し,他は--safeオプション付きで検証される.

別段ゴルフがメインではないのだが,Agda Golferなる生物が万が一にも生息しているかもわからないので,念のためあなごる・あなぷる同様コードサイズを出すようにした.ただし,コードサイズの定義がそれらとは少し違っており,Unicode文字を多用するAgdaに合わせてbyteカウントではなく文字カウントとし,また証明の可読性に配慮する余地を残すため改行('\r','\n')と空白(' ')をカウントしないようにしている.

Agdarが増えるよ!!やっt(ry

Agdaとフォントと微妙な設定

Agda書くとみんなフォントに困る…と,思う.Agda標準ライブラリの時点で_⊔_とか_⊎_とか⟨_⟩とか普通のランゲッジでは使わないような文字入りの演算が目白押しだ.当然下手なフォント設定すると正しく表示されない.プログラミングに適したmonospaceのフォントでこういうのをフォローできてるスケーラブルフォントが欲しい.

聞くところによるとWindowsユーザはWindowsインストーラ版を使うとあんまり気になることない(GNU unifontもワンセットで入るから?)らしい.でもGNU unifontってビットマップフォントのようだし,putty仮想マシンGentoo入ってX無しでサギョウ…という私のカッコイイスタイルでは使えない.そもそもWindowsインストーラ版のAgdaなど花拳繍腿.一度汚れたWindowsはもう二度と元には…元には…なので,あんまりWindowsに余計なもの入れたくはないし.

で,結局どうしてるかというところだが.ザックリDejaVu Sans MonoにMS UI GothicをFontLinkして使っている.

FontLinkとはWindowsのステキ機能()で,あるフォントに欠けてる文字を別に指定したフォントから持ってきてカバーするような機能だ.レジストリを弄る.HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink 以下にメインのフォント名の値を追加し,データとして"フォントファイル,フォント名,表示アスペクト比?(スケール,スケール)"という値を必要なだけ記述する.次のような感じ.

レジストリ: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink
値: DejaVu Sans Mono
種類: REG_MULTI_SZ
データ:
MSGOTHIC.TTC,MS UI Gothic,128,80

レジストリ弄ってるなら結局汚してるじゃねーかというのはもっともではあるが…汚されているんでなく汚してるんだし(震え声)

で,これ.主な問題は最後の数字2つの調整で,これらの値,何が正しくてどう調整すればいいのか自分には全くわからない(まあ,Windowsの話なのであんまり理解する気も無いのだが).メインのフォント(この場合DejaVu Sans Mono)に合わせるためのものなので,それによって設定されるべき値も違ってくる.下手な値にするとどうなるかというと,FontLineによって出張ってきた(MS UI Gothicの)文字がスゴイ横にツブれた文字になったり,上半分が切れて表示されたりとかする.しかもレジストリ値なので設定変たらリブート,それでもダメならまた変えてリブートと非常にメンドクサイ2分探索を強いられる.恐しいことにアプリケーションによっても影響が異なり,この設定はputty合わせで行ったが,sakura editorで見たらなんかダメだった(まぁ,これはCJK ambiguous charactersの扱いが違うとか別要因もあるかも).

他のひとたちはどうしてるんだろうというのは実際興味ある.たぶんnativeなlinuxとかmacとかなんだろうけどね.macもあんまりよく知らないけどMBAのiTerm上からMonacoで同様に仮想マシン入ってAgdaする分にはあんまりフォント問題無いようだし.

ghcjsをhardened profileのGentooにインストールするには

今年もよろしくおねがいします.


大晦日にghcjsのREADMEが更新され,ghc-7.6ベースになったり,これまで煩雑だったインストール作業がスクリプト化されたりするなど,いろいろと変更があった.実にはタイミングの悪いことに,このREADME更新が行われる直前,半年振り?くらいにghcjsをビルドしてみようか*1と作業し初めてしまっており,何かインストール手順に記載されてないパッケージに依存するようになってるなぁとかで試行錯誤したりしてイロイロとハマっていたのだが,なんのことはない,ちょっと大きな変更の過渡状態で作業していたと知ってガックリ.

で,例によって?hardened profileなGentooだとインストーラスクリプトそのまま走らせてもビルドエラーで入らないので,どうしたかについて備忘録として記載しておく.ちなみに実験環境はVirtualBox上のGentoo amd64 hardened.ちなみにdefault profileについてはインストール試していないが,もしそっちで何かダメでも同様の方法で動かせるようにはなるはずだろう.

基本はPIE/SSPビルドを無効にするにはどうすればよいですか?の項にある作業をghcjs公式のインストール手順・インストーラスクリプトに挿し込むことになる.ちなみにこの作業はghcをemergeする際にも行われている.ルートになって下記手順でghcビルド時のオプションがどうなるか確認できる.

ebuild /var/lib/layman/haskell/dev-lang/ghc/ghc-7.6.1.ebuild configure
cat /var/tmp/portage/dev-lang/ghc-7.6.1/work/ghc-7.6.1/mk/build.mk

build.mkの内容はたとえばこんなカンジになっている.

# Gentoo changes
docdir = /usr/share/doc/ghc-7.6.1
htmldir = /usr/share/doc/ghc-7.6.1
SRC_HC_OPTS+= -optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack
SRC_CC_OPTS+=-march=native -pipe -O2 -march=native -march=native -nopie -fno-stack-protector -Wa,--noexecstack -Wa,--noexecstack
SRC_LD_OPTS+= -nopie
BUILD_DOCBOOK_PDF  = NO
BUILD_DOCBOOK_PS   = NO
BUILD_DOCBOOK_HTML = NO
HADDOCK_DOCS       = NO
SRC_HC_OPTS+=-w

基本この条件をそのままghcjsビルド時にも与えてあげればよい.が,一般ユーザのHOME以下に一般ユーザの権限でビルドして入れるので,docdir/htmldirの指定はこのままだとマズい.消してしまおう.

また,SRC_HC_OPTSはインストーラスクリプトghcjs-build.sh内からcabalが呼ばれるときにも--ghc-optionsとして与えてあげる必要がある.(これは設定次第で不要になるんじゃないかなとも思っている*2のだが,追い切れていない.だれか知ってる人情報キボン)

cd
git clone https://github.com/ghcjs/ghc
cd ghc
git checkout ghc-7.6
./sync-all -r https://github.com/ghc get
./sync-all -r https://github.com/ghcjs --ghcjs get
./sync-all checkout ghc-7.6
cabal update
./unpack.sh
cat > mk/build.mk <<EOF
SRC_HC_OPTS+= -optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack
SRC_CC_OPTS+=-march=native -pipe -O2 -march=native -march=native -nopie -fno-stack-protector -Wa,--noexecstack -Wa,--noexecstack
SRC_LD_OPTS+= -nopie
BUILD_DOCBOOK_PDF  = NO
BUILD_DOCBOOK_PS   = NO
BUILD_DOCBOOK_HTML = NO
HADDOCK_DOCS       = NO
SRC_HC_OPTS+=-w
EOF
nano -w ghcjs-build.sh # cabal/cabal-meta に --ghc-options="SRC_HC_OPTSの内容"を追加する.
export PATH=$HOME/.cabal/bin:$PATH # ghcjs-build.shはこのパスが通っていることを前提としてるっぽい.
./ghcjs-build.sh

でもってこのインストールスクリプト,結構時間がかかるため,ある程度覚悟を決めるか寝るか別の作業をしよう.個人的には寝るのをお勧めする.仮想マシン環境とはいえ今回の実験環境は現時点ではそれなりにパワーのあるマシン(Intel(R) Core(TM) i7-3960X CPU @ 3.30GHzで仮想環境として割り振りはCPU8コア&メモリ8GB)と言えるほうだが,それでも以下のように1.5hかかっている.

./install-ghcjs.sh  9672.08s user 3072.23s system 234% cpu 1:30:29.01 total

通常何もしなければghcjsは使われないが,

export PATH=$HOME/ghcjs/bin:$PATH

するとghcjsが有効な状態になる.

一応これまででghcjsインストール完了ではあるのだが,webkitなども入らないことには十全にghcjsを使うことができないので,exampleまで入れる.というかそこにも罠があるのでそっちまで解説しなければ片手落ちだったりする.今回は事前にwebkit-gtk-1.8.3-r200をemergeしている.gtk+-2.24.12なのでcabal-meta install時に-fgtk3は無し,また,webkit-1.10系じゃないので-fwebkit1.8を付けている.

export PATH=$HOME/ghcjs/bin:$PATH
git clone https://github.com/ghcjs/ghcjs-examples.git
cd ghcjs-examples
cabal-meta install --force-reinstalls -fwebkit1-8 --ghc-options="-optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack"

とやっても,残念なことに失敗しているハズだ.これは gtk系hackage特有の問題?に遭遇している.gtk系hackageは通常のsetupの他にsetup-wrapperというものを作ってビルドを行うようになっているのだが,cabal installで--ghc-optionsに指定した内容が奥のほうまで届かないようなのだ.バグ臭い.

なので,一度周辺をクリアして, --ghc-options を強制的に挿し込むんでもらうようにコードを弄った上でリトライする.

rm -rf vendor/*/dist vendor/gtk2hs/*/dist
sed -i 's/, setupDir]/, setupDir, "-optc-march=native", "-opta-march=native", "-optc-nopie", "-optl-nopie", "-optc-fno-stack-protector", "-optc-Wa,--noexecstack", "-opta-Wa,--noexecstack"]/' vendor/gtk2hs/*/SetupWrapper.hs vendor/webkit/SetupWrapper.hs vendor/webkit-javascriptcore/SetupWrapper.hs
cabal-meta install --force-reinstalls -fwebkit1-8 --ghc-options="-optc-march=native -opta-march=native -optc-nopie -optl-nopie -optc-fno-stack-protector -optc-Wa,--noexecstack -opta-Wa,--noexecstack"

途中で

gtk2hsC2hs: UName: root name supply used after saving

が原因のエラーになることがあるが,気にせずもう一度叩くとキモいけど通るようになるとのことなのでそうすると確かにキモいけど何故か通る.

ghcjs-min ~/.cabal/bin/ghcjs-hello

で,~/.cabal/bin/ghcjs-hello.trampoline.jsexe 以下にJavaScriptが吐かれているのが確認できる.あとはコイツをブラウザから見える状態にしてやればよい.

ghcjs有効状態にしてcabal installでパッケージを入れると,各モジュール毎に対応するJavaScriptもモリモリ吐き出していく様は圧巻. cabal install --helpを見ると--enable/disable-java-scriptなどというオプションがあるのもおもしろい.

ghcjsはまだ発展途上だが,fay等と比べ,

  • 既存のパッケージが通常通りcabal installして使える
  • webkitによるStandalone BinaryでWebサーバに置かずとも動作を確認できる

など優れた点が多い,逆に,

  • FFIが弱い(=既存のJavaScriptライブラリは使いにくい)

など力弱い点もあるが,むしろunsafeで名状し難いJavaScript跳梁跋扈する深淵の覗き窓*3を作るくらいなら弱いままでも良いくらいという向きもあるだろう.とにかく,ghcjsの今後ますますのご発展を心よりお祈り申し上げます.

*1:だいたい[http://blog.konn-san.com/article/20121225/fay-introduction:title=こんさんの記事]のせい

*2:通常のcabalコマンドがこの指定を必要としていないので

*3:ああ!窓に!窓に!

fluent-logger-haskell作った

なんかイマサラだけど,少し前にfluent-loggerのHaskell版と,そのconduitインターフェース版を作った.

Haskellからfluentdにポンポンとイベントログ吐ける.

以下,process-conduitとcsv-conduitでdstatの出力を吐き出す*1例,

{-# LANGUAGE OverloadedStrings #-}
import Prelude hiding ( null )
import qualified Data.HashMap.Strict as HM
import Data.Conduit
import Data.Conduit.Process
import qualified Data.Conduit.List as CL
import Data.CSV.Conduit
import Network.Fluent.Logger
import Network.Fluent.Conduit
import Data.ByteString ( ByteString, null )
import Data.ByteString.Char8 ( unpack )
import Data.List ( groupBy )
import Data.Function

type Formatter a = [ByteString] -> [ByteString] -> [ByteString] -> a

settings :: FluentSettings
settings = defaultFluentSettings { fluentSettingsTag = "dstat"
                                 , fluentSettingsHost = "127.0.0.1"
                                 }

main :: IO ()
main = runResourceT $
       sourceDstat "/usr/bin/dstat" ["-cm"] =$=
       awaitForever (mapM_ yield) $$
       sinkFluent settings

sourceDstat :: MonadResource m =>
               FilePath
            -> [String]
            -> GSource m [ ( ByteString, HM.HashMap ByteString Double) ]
sourceDstat dstat opts = run cmd >+> toCSV >+> convert toDstatMap
    where
      run = sourceCmd . unwords
      cmd = dstat : opts ++ [ "--output", "/dev/fd/3"
                            , "3>&1", ">/dev/null", "2>/dev/null" ]

toCSV :: MonadResource m =>
         GInfConduit ByteString m [ByteString]
toCSV = injectLeftovers $ intoCSV defCSVSettings

convert :: Monad m =>
           Formatter o
        -> Pipe l [ByteString] o u m ()
convert format = CL.drop 5 >> withAwait (withAwait . convert' . fill)
    where
      convert' headers subs = withAwait $ \xs -> do
                                yield $ format headers subs xs
                                convert' headers subs

withAwait :: Monad m =>
             (i -> Pipe l i o u m ())
          -> Pipe l i o u m ()
withAwait f = await >>= maybe (return ()) f

toDstatMap :: Formatter [ (ByteString, HM.HashMap ByteString Double) ]
toDstatMap headers subs xs =
    [ ( fst $ head zs
      , HM.fromList $ map snd zs )
    | let ys = zip headers $ zip subs $ map (read . unpack) xs
    , zs <- groupBy ((==) `on` fst) ys
    ]

fill :: [ByteString] -> [ByteString]
fill = scanl1 (\x y -> if null y then x else y)

fluentd側で標準出力に出してみるとこんなカンジ,

...
2012-12-29 17:33:49 +0900: adding match pattern="dstat.**" type="stdout"
2012-12-29 17:33:49 +0900: listening fluent socket on 0.0.0.0:24224
2012-12-29 17:33:49 +0900: listening dRuby uri="druby://0.0.0.0:24230" object="Engine"
2012-12-29 17:34:00 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.005,"siq":0.003,"sys":0.099,"usr":0.169,"idl":99.723}
2012-12-29 17:34:00 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707342336.0,"free":3712630784.0}
2012-12-29 17:34:01 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.0,"usr":0.125,"idl":99.875}
2012-12-29 17:34:01 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707350528.0,"free":3712622592.0}
2012-12-29 17:34:02 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.0,"usr":0.0,"idl":100.0}
2012-12-29 17:34:02 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707604480.0,"free":3712368640.0}
2012-12-29 17:34:03 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.124,"usr":0.0,"idl":99.876}
2012-12-29 17:34:03 +0900 dstat.memory usage: {"buff":349925376.0,"cach":3611164672.0,"used":707604480.0,"free":3712368640.0}
2012-12-29 17:34:04 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.126,"siq":0.126,"sys":0.0,"usr":0.0,"idl":99.748}
2012-12-29 17:34:04 +0900 dstat.memory usage: {"buff":349929472.0,"cach":3611160576.0,"used":707604480.0,"free":3712368640.0}
2012-12-29 17:34:05 +0900 dstat.total cpu usage: {"hiq":0.0,"wai":0.0,"siq":0.0,"sys":0.0,"usr":0.125,"idl":99.875}
2012-12-29 17:34:05 +0900 dstat.memory usage: {"buff":349929472.0,"cach":3611160576.0,"used":707604480.0,"free":3712368640.0}

動確取ってるのは現在のhaskell-platformに積んであるghc-7.4だが,今現在hackageはghc-7.6でビルドしようとするので,依存してるmsgpackがTHガラミか何かでBuild failureになってしまう.そういうときドキュメントが生成してくれないのがhackageのちょっと悲しいトコロ.

*1:それfluent-plugin-dstatで(ry