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に貼っておきましょう。
[![CircleCI](https://circleci.com/gh/ユーザ名/レポジトリ.svg?style=svg)](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でいい気がしました。