チームでRustの開発をする

こんにちは。κeenです。 サービス開発チームに所属しており、リリースに向けて日々RustでWebサービスを開発しています。 サービス開発チームはサーバサイドとデバイスエージェント、フルタイムとパートタイム合わせて5人でRustを書いています。 あまりRustをチームで使っている事例を聞かないのでIdeinの社内での環境を共有したいと思います。

ローカル環境

Rustのコンパイラは出来る限りstableを使っています。 全員で同じRustコンパイラを使うためにrust-toolchainファイルにRustのバージョンを直接指定しています。 今だと"1.26.2"になっています。最初は"stable"と指定していたのですが上流でリリースされると勝手にバージョンが上がるのがCIなどで具合が良くなかったのでバージョンを指定する方式にしました。 新たにstableが出る度に手で更新しています。

また、まだプレビュー版ですがstableのツールチェーンでrustfmtが使えるので各自で使うようにしています。 こういうフォーマッタは全員で同じ結果になるのが重要なのでエディタ独自のフォーマットではなく統一されたツールを使います。 それぞれ使い方はあるかと思いますがたとえば私はEmacsに設定してファイルを保存する度にフォーマットするようにしています。 フォーマットはデフォルトのフォーマットのままです。後述しますがCIでも検査しています。

開発にpostgresqlのクライアントライブラリやdieselのCLIなども必要ですが各自でインストールするようにしています。 このあたりはCIでも必要になるのでdockerイメージを作った方がいいかもしれません。

CI環境

Circle CIを使っています。 Circle CIを選んだ理由が特にある訳ではなくて、既に会社で契約していたからそのまま使っただけです。

コンパイル/テスト

CIではRustコンパイラが入ったDockerイメージを作ってコンパイル/テストしています。 Rust公式のDockerイメージもありますがrustfmtを入れたりいくつかのパッケージをインストールしたりしているので独自のイメージを使っています。 ここでもRustのバージョンはタグで固定していて、 rust-stable:bionic-1.26.2 のようにタグを打っています。

その他のコンパイル/テストのコマンドは普通の cargo test で行っており特に変わったことはしていません。

ツール

dieselのCLIツールなどをインストールする必要があります。 これは cargo install でインストールしていますが「特定のバージョンがインストールされていなければ上書きインストールを、されていれば何もしない」の挙動ができないので軽いシェルスクリプトでラップして使っています。 概ね以下のシェル関数で機能しています。

cargo_install_if_need() {
    crate="$1"
    version="$2"
    shift 2

    cargo install --list | grep "$crate v$version" > /dev/null ||
        cargo install "$crate" --vers "$version" "$@" --force
}

また、rustfmtによるフォーマットの検査もしています。正しくフォーマットされていないとdiffを吐いてCIを終了します。 概ね以下のような形です。

cargo fmt --all -- --write-mode diff
exit $?

キャッシュ

最初は Cargo.toml をキーにキャッシュしていましたがちょくちょく問題が出たので今はコンパイラやOSバージョンなどもキーに入れています。 Rustのクレートの中にはコンパイル時の環境でコンパイル内容を変えたりするものもあるのでOSのバージョンも(もうちょっというとインストール済みパッケージも)キーに必要です。

キャッシュ対象はアプリケーションに ~/.cargo/registry/target 、CLIツールに ~/.cargo/bin/~/.cargo/.crates.toml をキャッシュしています。

パッケージング

テスト済みのソースコードをパッケージングします。 プロファイルがtestではないのでテストとは別にコンパイルし直さないといけないのが少し辛いところですね。

サーバのコードはdockerに乗せてデプロイするので拙作のcargo-pack-dockerでパッケージングしています。荒削りなツールですがあまり複雑なことをしてないので特に不満なく使えています。

デバイスエージェントは最終的にはARMアーキテクチャであるRaspberry Pi上で動くのでクロスコンパイルが必要です。 Rustのクロスコンパイル用のコンポーネントやバイナリツールをインストールしてあるdockerイメージを用意してその中でクロスコンパイルしています。 .cargo/configにリンカを指定したりして比較的手軽にコンパイルできるようになっているようです。

開発フロー

まとまった人数がいるので普通にGitHubでチーム開発をしています。人数がいるのでレビューも特に支障なく行われています。 ここはRustだからといって特別なことはないようです。

Rustは学習難易度が高いと言われますがRust初心者で入ったメンバー(例えば @eldesh)も支障なく開発できているので業務利用するにあたって相対的に重要度は低いようです。

たまにライブラリに足りない機能などがありますが大抵OSSなので自分でPRを送れば解決します。

トラブルなど

最後に、たまに開発上困ったことが起きるのでいくつかを紹介します。

OSのバージョンを上げて実行時エラー

これはCIでのビルドには元のままのUbuntu16.04を、実行用のDockerイメージはバージョンを上げてUbuntu 18.04を使っていたがために起きたエラーでした。 ビルド時に存在していたDLLが実行時には(バージョンが上がって)別名になっていたので見つからずにエラーになっていました。

ARMのクロスコンパイルコンポーネントがインストールできない

Rust 1.26.0でのバグです。Tier 2のインストールが失敗するようになっていました。 このバグは1.26.1で修正されましたがそれまではRustのバージョンを更新できずにいました。

バイナリクレートの依存ライブラリが最新コンパイラの機能を使い始めた

中々不運が重なった事故でした。 上記のコンパイラバージョンを変更できない期間に、cargo install しているクレートの依存しているクレートが最新コンパイラの機能を使い始めました。 普段のCargoでのビルドならば Cargo.lock があるので新しいマイナーバージョンで新しい機能を使い始めても支障ありません(参考)。 しかしながら今回のcargo installのケースでは Cargo.lock が存在しなかったため(最近入りました)、ある日を境にCIがコケ始めました。

依存ライブラリがマイナーバージョン変更で壊れた

具体的にはchrono 0.3.1です。依存解決に失敗するようになってプロジェクト全体がコンパイルできなくなりました。 作者も破壊的変更であるとは認識していたものの、想像以上に影響範囲が広かったようですぐにyankされ、0.4.0が出ました。

終わりに

チーム開発といいつつそんなに特別なことはなく、CIなど足回りをしっかりしてあげればあとは普通に出来るようです。 それでもまだ試行錯誤は必要なので知見のある方がいましたら是非共有して下さい。

最後になりますが、Ideinでは一緒にRustでサービス開発をしてくれるメンバーを募集しています https://idein.jp/career 奮って応募下さい。