• 作成:
  • 更新:

RustプロジェクトでCircleCIを設定する

RustプロジェクトについにCircleCIを導入する余裕が生まれてきたので, 設定メモを書きます.

ググったのですが全網羅して書いてる所が見つからなかったので仕方なく自分で調べて書いてます.

CircleCIのimageを利用する

最初はRust公式のDockerイメージ rust - Docker Hub を使おうと思ったのですが,

CircleCI的にはCircleCIの作ってる circleci/rust - Docker Hub を推奨してるらしいですね.

それでタグは何を選択すれば良いんでしょう… Tags (344)とか探す気がしない. 少なくとも今回はJavaScriptは一切関係してこないので, nodeだのbrowserだのは要らないのですが… デフォルトのタグ無しを選択することにしました.

コンポーネントをインストールする

CircleCIのrustイメージにはclippyとrustfmtが含まれていないので

command: rustup component add clippy rustfmt

する必要があります.

rustfmtでフォーマットチェックする

ビルドには時間がかかるのでこちらを先に行います.

command: cargo fmt -- --check

で失敗した時返り値をエラーにしてdiffも出せます.

Building a Rust project on CircleCI にはcargo fmt -- --write-mode=diffとか書かれてますが多分古いバージョンですね.

ビルドする

普段のチェックならcargo checkでビルドせずにチェックだけ行いたいのですが, 今回はtestもちゃんと行うので結局ビルドするしか無さそうです.

またclippyも表層的な部分だけではなく深い所まで見るので結局ビルドを必要としています.

依存ライブラリが壊れている場合をステップ分けしたいので, 依存ライブラリだけ先にビルドしたいのですが, やっぱりそのオプションはまだ無いみたいですね…

素直にcommand: cargo buildするしか無さそうです.

clippyでチェックする

clippyはwarn程度のものだと返り値がエラーにならないので

command: cargo clippy -- -D warnings

とする必要があります.

testする

普通にcargo testが動きます.

やっぱりローカルもstableにする

CIのデフォルトツールチェインはstableなのでローカル開発環境もstableに合わせてしまいましょう. CIをnightlyにする勇気はありませんでした.

キャッシュする

そのままだとビルドするたびに全てのコンポーネントをビルドするので, ビルドが毎回遅くなります.

キャッシュを使いましょう.

依存関係のキャッシュ - CircleCI を読んで考えます.

キャッシュするべきなのは

  • rustupのコンポーネント(rustupの短縮)
  • プロジェクトの依存(cargo buildの短縮)

の2つですね.

キャッシュされてさえいれば, 自動でキャッシュが有効化されて特別な操作無しにキャッシュを利用してくれるはずです.

rustupのコンポーネントがキャッシュ有効かはrustupのバージョンを見れば良いでしょう.

コマンド実行の結果をキーにする簡単な方法がわからない… ので一度ファイルに書き出してチェックサムを見るという非効率っぽい方法を取らざるを得ませんでした.

差分ビルドはまあ今回はそこまで大きいプロジェクトではないのでやらなくて良いですかね… ファイルのチェックサムを全て取るとかやればtargetを保存するだけで良いのですが, それはそれで面倒なので今回キャッシュするのは依存ライブラリだけです.

注意が必要なのは, 普通cargoで入れたファイルのキャッシュは ~/.cargoに入りますが, CircleCI環境, というかRustのDocker環境の場合 /usr/local/cargo/registryに入るそうです.

参考:

rustupは/usr/local/rustupですね.

と思ってrustup component addもキャッシュしようとしていたのですが, /usr/local/rustup/全体をcacheしてしまうとOperation not permittedになってしまいますが, 全体をcacheしないとcacheの意味がないこと, rustup component addの実行に1秒ぐらいしかかからないことからrustup component addはキャッシュしないことにしました.

そして出来たのが以下です.

version: 2
jobs:
  build:
    docker:
      - image: circleci/rust
    steps:
      - checkout
      - run:
          name: rustup version
          command: rustup --version
      - run:
          name: rustup component add
          command: rustup component add clippy rustfmt
      - run:
          name: fmt
          command: cargo fmt -- --check
      - restore_cache:
          keys:
            - v1-cargo-lock-{{ checksum "Cargo.lock" }}
      - run:
          name: build
          command: cargo build
      - run:
          name: lint
          command: cargo clippy -- -D warnings
      - save_cache:
          key: v1-cargo-lock-{{ checksum "Cargo.lock" }}
          paths:
            - "/usr/local/cargo/registry"
            - "target"
      - run:
          name: test
          command: cargo test

ワークフローを使って分割する(失敗)

これまで全て同じbuildジョブに書いていきましたが, fmtは無条件で実行可能で, clippy, testはbuildさえ完了していれば可能なのですよね.

これからこのプロジェクトがそんなに大きく膨らんで, 分割しないとCI時間がやばいことになるプロジェクトになることはあまり想定されませんが, 今後他のRustプロジェクトにも使い回せるconfig.ymlを目指して分割してみることにしました.

Using Workflows to Schedule Jobs - CircleCI CircleCI2.0のWorkflowを試してみる - Qiita を参考に分割しました.

しかし

  • 1つずつDockerイメージを起動するのでそのオーバーヘッドがバカにならないこと
  • 分割できてもfmtとbuildが同時に出来てclippyとtestが同時に出来るぐらいで大したメリットがないこと
  • なんかキャッシュの復元が出来なかった(解決済み?)
  • 今はこんなことをやっている場合ではない

ことから没になりました.

巨大なRustプロジェクトをCircleCIに突っ込む時は思い出して分割しようと思います.