RでCIというと、ほとんどはTravis CIが使われます。なんといっても、
devtools::use_travis()
とするだけで準備が整うというお膳立てっぷりです。
でも、Travis CIの弱点はCIに使われるイメージが古いことです。最近ようやくデフォルトイメージがUbuntu 14.04になりましたが、最先端を追い求める人々にはつらいです。
その点、CircleCIが便利なのは任意のDockerイメージを使ってCIを回せることです。他のライブラリに依存することがけっこう多いRでは使いどころがありそうなので、ちょっとやり方を調べてみました。
Travis CIのソースを覗く
そもそもTravis CIはlanguage: rを指定したとき、具体的にどんな処理をしてくれているのでしょう。ちょこっとソースを見ておきます。
具体的にはこのへんです。
def script
# Build the package
sh.if '! -e DESCRIPTION' do
sh.failure "No DESCRIPTION file found, user must supply their own install and script steps"
end
tarball_script =
'$version = $1 if (/^Version:\s(\S+)/);'\
'$package = $1 if (/^Package:\s*(\S+)/);'\
'END { print "${package}_$version.tar.gz" }'\
sh.export 'PKG_TARBALL', "$(perl -ne '#{tarball_script}' DESCRIPTION)", echo: false
sh.fold 'R-build' do
sh.echo 'Building package', ansi: :yellow
sh.echo "Building with: R CMD build ${R_BUILD_ARGS}"
sh.cmd "R CMD build #{config[:r_build_args]} .",
assert: true
end
# Check the package
sh.fold 'R-check' do
sh.echo 'Checking package', ansi: :yellow
# Test the package
sh.echo 'Checking with: R CMD check "${PKG_TARBALL}" '\
"#{config[:r_check_args]}"
sh.cmd "R CMD check \"${PKG_TARBALL}\" #{config[:r_check_args]}; "\
"CHECK_RET=$?", assert: false
end
export_rcheck_dir
if @devtools_installed
# Output check summary
sh.cmd 'Rscript -e "message(devtools::check_failures(path = \"${RCHECK_DIR}\"))"', echo: false
end
# Build fails if R CMD check fails
sh.if '$CHECK_RET -ne 0' do
dump_logs
sh.failure 'R CMD check failed'
end
このあともうちょっと処理が続きますが、簡単にするためこの辺までで止めておきます。基本的にやっていることは、
- DESCRIPTIONファイルが存在するかチェック
- DESCRIPTIONファイルからパッケージ名とバージョンを抜き出してきて環境変数に指定
R CMD build .を実行R CMD check ${PKG_TARBALL}を実行Rscript -e "message(devtools::check_failures(path = \"${RCHECK_DIR}\"))"を実行
という感じです。順を追ってこれをCircleCIでどう書くか見ていきましょう。
CircleCIの基本
詳しい説明は省きますが、.circleci/というディレクトリの下にconfig.ymlという名前の設定ファイルを置きます。簡単なやつだとこんな感じです。
version: 2 jobs: build: docker: - image: rocker/geospatial:latest steps: - checkout - run: R CMD build .
versionはCircleCIのバージョンです。バージョン2を使います。
jobsには実行するジョブを並べます。この下には、後述するWorkflowを使う場合は任意の名前のジョブを指定しますが、使わない場合はbuildという名前固定です。
buildの下に指定しているdockerはCIに使うDockerイメージを指定し、stepsには実行する処理を記述します。
stepsにはコマンドを実行するrun、レポジトリをチェックアウトするcheckout、アーティファクトを保存するstore_artifactsとかがあります。
詳しくは公式ドキュメントとかをご参照ください。
Travis CIがやってた処理を再現
DESCRIPTIONファイルが存在するかチェック
まあなかったらあとでエラーになるので別にいいか、ということでスキップします。
DESCRIPTIONファイルからパッケージ名とバージョンを抜き出してきて環境変数に指定
ここはいきなりちょっと難しいんですが、こんな感じです。
- run: | Rscript --vanilla \ -e 'dsc <- read.dcf("DESCRIPTION")' \ -e 'cat(sprintf("export PKG_TARBALL=%s_%s.tar.gz\n", dsc[,"Package"], dsc[,"Version"]))' \ -e 'cat(sprintf("export RCHECK_DIR=%s.Rcheck\n", dsc[,"Package"]))' \ >> ${BASH_ENV}
${BASH_ENV}は、環境変数を保管しているファイルへのパスです。ここにexport FOO=barと追記しておけば、以後のステップでその環境変数が設定されるようになります。
Travis CIは謎のPerlスクリプトを使ってDESCRIPTIONから情報を抜き出していましたが、ここはせっかくなのでRでやってみます。read.dcf()はDebian Control Files、つまりDESCRIPTIONみたいなファイルを読み取る関数です。
PKG_TARBALLはRのパッケージをビルドした後のtarファイル名です。RCHECK_DIRはチェックを流したときのログが保管されるディレクトリです。
ビルドとテストを実行
ここは単純にコマンドを流すだけです。
- run: R CMD build . - run: R CMD check "${PKG_TARBALL}" --as-cran --no-manual - run: Rscript -e "message(devtools::check_failures(path = '${RCHECK_DIR}'))"
テストのログをアーティファクトとして保管
ここがちょっと難しいところです。アーティファクトはstore_artifactsというディレクティブに指定するんですが、ここではパスに環境変数を使ったりすることができません。こんな感じに固定のパスになります。
- store_artifacts: path: /tmp/Rcheck when: always
なので、この前にこの固定のパスにRCHECK_DIRを移動させてきましょう。
- run: command: mv ${RCHECK_DIR} /tmp/Rcheck when: always
ちなみに、when: alwaysというのはこれまでのコマンドが成功しても失敗しても必ず実行する、という指定です。デフォルトはon_successなのでこれを指定しておかないとエラーになったときのログとかが見れません。
ここまでのまとめ
基本的にはこんな感じでしょう。(ちょっとここまでの説明と順番が違っていたりしますが気にしないでください)
badgeを付ける
これをREADMEに貼っておきましょう。
[](https://circleci.com/gh/ユーザ名/レポジトリ)
Workflowを使う
上のconfig.ymlだと1つのバージョンしかテストできません。Travis CIだと、
r: - release - devel - oldrel
とかいう感じでマトリクスビルドができましたが、CircleCIだとWorkflowというのを使うらしいです。
ドキュメントを見ても&とか<<:の使い方についてちらっとしか書かれていないのでよく分からないんですが、私はこんな感じになりました。いろいろ書き方はあるみたいです。defaults(ここではdefaultにしているというだけで、任意のキー名が使えます)には共通の部分(ビルド、テストの処理は同じ)を指定して、各jobsには変わる部分(イメージはそれぞれ違う)を指定します。それをworkflowで並べて書くという感じです。
あんまり理解できてないのでもっといい書き方あれば教えてください。
defaults: &defaults steps: # setup - checkout - run: name: Set environmental variables command: | Rscript --vanilla \ -e 'dsc <- read.dcf("DESCRIPTION")' \ -e 'cat(sprintf("export PKG_TARBALL=%s_%s.tar.gz\n", dsc[,"Package"], dsc[,"Version"]))' \ -e 'cat(sprintf("export RCHECK_DIR=%s.Rcheck\n", dsc[,"Package"]))' \ >> ${BASH_ENV} # build and test - run: R CMD build . - run: R CMD check "${PKG_TARBALL}" --as-cran --no-manual - run: Rscript -e "message(devtools::check_failures(path = '${RCHECK_DIR}'))" # store artifacts - run: command: mv ${RCHECK_DIR} /tmp/Rcheck when: always - store_artifacts: path: /tmp/Rcheck when: always version: 2 jobs: "r-release": docker: - image: rocker/geospatial:latest <<: *defaults "r-devel": docker: - image: rocker/geospatial:devel <<: *defaults workflows: version: 2 build_and_test: jobs: - "r-release" - "r-devel"
まとめ
CircleCIわりと普通に使えそうでした。ただし、Dockerのイメージキャッシュは有料オプションになってしまったので、思ったほどは高速化されないかもしれません。
Travis CIでsudo: requiredを使わないといけなくてテストが長時間かかって悩んでいるなら移行を検討すべきな気がしますが、Rユーザ的にはやはりTravis CIが知見がたまっているので、そんなに困ってなければTravis CIでいい気がしました。