• 作成:

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ですね.

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

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

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

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

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

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

しかし初回は

ワークフロー
ワークフロー

のようにうまく行くのですが,何故かキャッシュのリストアがOperation not permittedでうまく行きません.

また

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

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

うまく行かなかった設定ファイルを以下に書いておきます.巨大なRustプロジェクトをCircleCIに突っ込む時は思い出して頑張って問題を解決しようと思います.後誰か解決方法を知ってたら教えてください.

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

workflows:
  version: 2
  all:
    jobs:
      - init
      - build:
          requires:
            - init
      - fmt:
          requires:
            - init
      - clippy:
          requires:
            - build
      - test:
          requires:
            - build