各種Scheme処理系をまとめてDocker上にビルドする

yuniはR6RS/R7RSで移植性を担保するためのライブラリなので、そのテストも当然各種のScheme処理系でやらないと意味がない。
... というわけで各種Scheme処理系をトラックするためのリポジトリとビルドスクリプトを用意しました。

元々のyuniの目的に各種Scheme処理系の第三者テストをするというものが有るのでこの辺の苦労もプロジェクトのうちでは有るんですが、予想よりずっと大変なのでどうしたもんかと考え中。
イメージの使い方等はまた別途。。
率直に言って、まだ一般には役に立たないです。役に立つとすれば、Dockerfileにビルド依存を全部書いたので、何をapt-getすればこれらの処理系がソースからビルドできるのかが解る点ですかね。。

# Install required packages:
#
#  buildsystem: cmake gcc g++
#  Gauche: autoconf automake
#  NMosh: libgmp-dev libonig-dev
#  Guile: libtool flex gettext pkg-config libunistring-dev libffi-dev libgc-dev
#  Chicken: time (avoid bashism)
#  Vicare: texinfo

なぜDockerイメージなのか

そもそも、みんなどうやってポータブルなSchemeライブラリを書いているのかはちょっと謎ですが、いろいろ考えた結果Dockerで管理するのが一番簡単という結論に至りました。

  • 独自パッケージシステムの一般化

個人的には"Scheme共通のパッケージシステム"の実現はかなり難しいと思っています。yuniのモデル - 移植性の高い基盤ライブラリを設計してライブラリをその上で書かせ、ある程度以上のサイズの機能はC FFIに頼る - に比べて、R7RSはsmall仕様であってもコンセンサスを得づらい仕様がいくつか有り(char-ready?とか)、R7RS前提のライブラリ作りはうまくいかないと個人的には考えています。
というわけで、各種Schemeの独自パッケージシステム(eggとかsnowとかRacketのパッケージ等)にyuniの基盤ライブラリをそれぞれ載せる必要があり、それをテストするためには各種処理系から一旦stable版のライブラリを抜かないといけないので生活に困るという問題が有ります。
要するに普段使ってるRacket環境をテストのために壊したくないんです!
実際にはRacketは複数の異バージョンインストールを問題無く管理できますが、POSIX上のNMoshのようにこれを想定していない実装もあるので。

  • 再現性

yuniの実装が進むにつれて、問題の再現を他の人の手元でも行う必要が出てくると思います。このとき、当然テストに使ったリビジョンを管理しないといけないですが、それ以外にディストリビューションによって挙動が違うという現象も普通に発生するため、問題を再現する人の手元で可能な限りリビルドをさせない考察が必要になります。
... もっとも、Dockerは未だそれほど支持されているとは言い難い面があるので、暫くは"Ubuntu14.04だと動くけど15.10では動かない"といった比較をやりやすくするのがDockerベースにする最大の理由だと思います。

  • 移植性

例えばChickenはFedoraコンパイラ(gcc 5.x)だと常識的な時間でテストが終わらない事が有りますが、そういうテストは従来Ubuntuユーザには面倒なものでした。DockerならFedora環境をUbuntu上に作るのも非常に簡単なので、メジャーなディストリビューションを全方位でカバーできます。
yunibaseでは、そのうちMusl libcを採用したディストリビューション(alpine)とかuclibcも足そうと思っています。

インフラの問題

最初はTravisで十分だろと思っていたんですが、dockerイメージの生成に予想以上に時間が掛かりFOSS無料枠の50分*1じゃ足りないという問題が。。というわけでおうちでビルドしたものをDocker Hubに上げるCMakeLists.txtを用意しました。

ライセンスとかどうすんのか(/docにインストールしたのがsubstantial portionsなのか)という問題は有るものの、普通のパッケージシステムでも問題としては同じなのでコミュニティコンセンサスであるという見解。
そのうち専用のVPSを借りて自動化しようと思います。(現在は手元でシェルスクリプトを実行して一晩寝る必要がある)
より難しいのはWindowsや他のOSで、WindowsのDocker上ビルドはまだ成功してないし、他のOSに至ってはDockerが無いのでchroot jailでどうにかする必要があるというかなりスパルタンな状況。

ビルドシステムの仕組み

で、やること自体は非常に簡単で:

  • ビルドステップ記述(Recipe)を書く
  • 登録する

でビルド→テスト→インストールまでまとめてできるCMakeプロジェクトを作りました。(このシステム自体をSchemeで書くとブートストラップ問題が起こるので避けた & シェルスクリプトデバッグが面倒だった)
Recipeは非常に単純なフォーマットで:

set(RECIPE_GAUCHE # Recipe for Gauche 
    STEP "Configure" "./configure" "--prefix=__INSTALL_PREFIX__"
    STEP "Build"     MAKE __MAKE_OPTS__
    STEP "Test"      MAKE check
    STEP "Install"   MAKE install
)

set(BOOTSTRAP_GAUCHE # Recipe for Gauche 
    STEP "Bootstrap" "./DIST" gen
)

list(APPEND BOOTSTRAP_GAUCHE ${RECIPE_GAUCHE})

実行すべきコマンドにクラスコードを振って列挙するだけです。クラスコードはSTEPの後ろに書いてある"Configure"とか"Test"のことで、これによってTestコマンドだけ失敗を許可したりできます。
そのうち"Build"コマンドをリプレイすることで警告を収集したりstatic analysisを実行するといった機能も統合予定。ビルドリプレイは非常に便利で、これ無しではC/C++のコードを書きたくなくなります(誇張)。

ソースコードの管理

ソースコードはcurrentとstableに分け、全てをGitのリポジトリに入れて管理することにしました。stable側は.tar.gz形式で持っても良いかもしれないですが、diffを取りやすいので展開した形でGitに入れました。currentは単にGitのsubmoduleです。
current/stableの区別は:

という区別で、今のところcurrentのビルドに不要ならばstableは追わないというポリシにしています。安定してきたらstableも追うように変えるかも。
この方針でdropになった処理系はLarcenyで、Larcenyはリリースバイナリが無いとbootstrapできないという一頃のSmalltalkのような状況になっているので現状のyunibaseの仕組みではカバーできませんでした。(追記: 冷静に考えるとbootstrap後のソースを入れれば良い気もしますね。。それでもLinux限定になっちゃうと思うけど)

各種処理系の問題

で、実際に各種処理系を載せてビルドしてみると、結果的にそれだけで休日を潰す大工事になってしまいました。。

  • ソースの日付に敏感な実装 - Gauche, NMosh

GaucheやNMoshはbootstrapルールがMakefileに残ったまま出荷されるので、リリースビルドであってもシステムにGaucheがインストールされているとそれを使ってしまう可能性があります。
こういう仕様だと問題になるのはGitはファイルの変更日付はトラックしないという制約で、特に打率の低かったNMoshについてはビルド前にソース側をtouchするという特別対応を入れています。

  • Dockerのvolume上だとビルドできなくなる実装 - Gauche

理由は全くの謎でgccが自然に失敗する。たぶんですがこれもタイムスタンプ問題でビルド中に意図せず再生成が走っているのではないかと思っています。現状はvolume上にソースは置かず、一旦ソースツリーをイメージ内にコピーしてからビルドするデザインに変更。

  • Docker上だとテストが通らなくなる実装 - NMosh, Guile

Dockerは微妙に特殊なファイルシステムを使ってツリーのイメージ化を実装しているので、NMoshやGuileのPOSIXテストは失敗します。(ファイルを440のようなパーミッションに変更できない可能性がある)

  • ネットワーク経由でライブラリを拾う実装 - Racket, chicken

Racketとchickenはyuniが動作するためには本体のソースコード以外にパッケージが必要ですが、これらを外部から拾っているのでビルドに再現性が有りません。現状ではこの点は諦めて毎回拾ってます。つまり、外部と通信できない環境でのビルドも不可。
どうにかして依存ライブラリもsubmoduleとして足さないといけないけど良い方法が思いつかず。。yuniの方をコア言語だけで動くように変えるのもアリかな。。

  • Headを本気で追い掛けないとライブラリの要求バージョンに追い越される実装 - Racket

更に、Racketはリリースバージョンしかブランチが存在せず、周辺ライブラリも普通に依存バージョンのアップデートが行われるので

Resolving "racket-lib" via https://pkgs.racket-lang.org
Using cached14530178151453017815868 for git://github.com/plt/racket/?path=pkgs%2Fracket-lib

raco pkg install: version mismatch for dependency
for package: base
mismatch packages:
racket (have 6.4.0.3, need 6.4.0.4)

のように自然に壊れることになります。
ちなみにchickenはその辺が厳密でないので問題は現状表面化しません。

  • bootstrapに異常に時間が掛かる処理系 - Guile

他の処理系を合わせた全ビルド時間よりも時間が掛かるってどういうことだよ!
(実は最適化切ったりできるんですかね。。 - 調べてない)

ToDo

そもそも、まだyuni側のテストに統合していないのでそれが最優先。。
もう少し巾広いlibcをカバーしたいので、Muslとuclibcは入れられないかと思っています。他にサポートしたいのは優先度順に:

といった感じ。
ちなみに、yunibaseは個人的に制作中のゲームのビルドシステム再構築計画の一端で、ほぼ同じ概念をゲームのビルドにも使おうと思っています。普通はゲームエンジンクロスプラットフォームビルドの面倒を見てくれるんですが、その辺のノウハウはどうしても作ってみないと得られないので。

*1:フルシステムを占拠する場合の数字