はじめに
こんにちは、ディベロッパーエクスペリエンス開発チームのJungです。
この記事では2年以上 LINE iOSのビルドシステムとして運用したBazelをやめることにした背景についてご紹介します。
Bazel導入とこれまでのLINE iOS
LINE iOS は200万行以上のソースコードと200以上のモジュールで構成される大規模プロジェクトです。
LINE iOSのソースコードとモジュールの数が増えて規模を拡大し続けるにつれて、ビルド/テストの遅延と DX(デベロッパーエクスペリエンス) の低下という避けられない問題に直面し、「ビルド速度」と「ビルドの再現性」は大きな課題となっていました。
LINEはこの問題を解決するために Bazel というビルドシステムを導入しました。
ビルド環境の変遷
- 2019年:Bazelのルールセット、各モジュールのBUILDファイル作成など、移行に必要な実装と検証を実施
- 2020年:CIビルド環境と共にローカルのデバッグ環境まで Xcodebuild(Xcode の組み込みビルドシステム) から Bazel に完全移行
- 2021年:シェルスクリプト、Rubyスクリプト、Fastlaneなど、断片化されていたビルドフェーズを Bazel のルールセットに統合
- 2022年:Default ビルドシステムを Bazel から Xcodebiuld へ戻すことに決定
DXを考慮しながらビルドシステムを移行することは多くの努力と時間のかかる作業でしたが、移行の結果、ビルド速度を2倍にするといった効果を得ました。
しかし、メリットを得る一方で、ビルドシステムのメンテナンスコストの増加とDXの低下などの課題に直面することになりました。結果、2022年夏これ以上Bazelをデフォルトのビルドシステムとして使用しないことを決定しました。
2年以上 LINE iOS のメインビルドシステムとして使用していたBazelを最終的にやめることになった経緯についてこの記事で解説します。
ただし、この記事は LINEが Bazelを運用した期間の経験を元にします。Bazelの最新バージョンとは異なる点がある可能性があるのでご注意ください。
Bazelの利点
Bazelとは
Bazel とは Google が社内で使ってた物をオープンソース化したプロダクトで Make、Maven、Gradle のようなビルドツールです。
Bazelのホームページに行くと、まず最初に「{ Fast, Correct } - Choose two」というキャッチフレーズが目立ちます。いわゆる “ビルド時間”と“再現性” 両方の性能に優れたビルドツールと言われています。
Bazelのキャッチフレーズ
なぜ速いか
ビルドシステムとして正確さ(Correct)とは Input が同じなら何回ビルドしても同じ Output になる、つまり再現性を意味します。
Bazel は主に2つの手法で高い再現性を実現します。1つは、ビルドグラフのすべてのモジュールに対するすべての入力を追跡すること、もう1つは各モジュールごとにサンドボックスを作成して密閉された環境でビルドすることです。
しかし、Bazelはこの正確さを実現するために他のビルドツールより多くのことをやります。そのため、実はBazelの正確さモデルはビルド速度を低下する問題となります。
Bazel の正確さによって速度が低下する場合、Bazelはどのように正確さと速度を両立しているのでしょうか?
公式サイトではBazel を「高速で正確」と紹介していますが、厳密に言うと「正確さによって高速」と言う方が正しいかも知れません。なぜなら正確さが確保できたからこそ、分散キャッシング、リモート実行、並列実行など、さらなるビルド高速化技術が使えるようになるからです。
Bazel の正確さによって、クラウドサーバーに細かい単位でキャッシュされたビルド成果物はローカルマシーンとCIマシーンのビルド時に正しく再利用でき、高速化を実現します。
CI環境のBazelビルドは速い
Bazelのビルド高速化技術により、特にプロジェクト全体をリビルドする場合はビルドが大幅に高速化されます。
CIビルドの場合はブランチ切り替えでプロジェクトを再構築し、Cleanビルドが行うのでリモートキャッシュが最も活用できるケースになります。
2020年に集計した下記のチャートでは LINE iOSが Bazelに移行後のCIビルド時間の変化が見られます。
※ Bazelに移行後のCIビルド時間の変化
ローカル環境のBazelビルドはそうでもない
ではBazelはローカル環境でのビルドも速いでしょうか?
ローカルビルドはプロジェクト再構築が頻繁に発生するCIビルドとは違って、ほとんどのエンジニアはプロジェクトを段階的にコンパイルします。つまり最初ビルドだけリモートキャッシュの効果を享受した上でビルド済みの状態から作業を開始し、プロジェクトの一部だけ変更して再ビルドします。
ビルドシステムはこの時に「変更したものだけリビルト」して高速化を実現します。その技術を「インクリメンタルビルド」と言います。
インクリメンタルビルドは編集、コンパイル、実行、フィードバックと言った開発ループに直接影響するため、再ビルドが遅いと開発コストの増加につながります。
また、再ビルドが遅いほどエンジニアのコンテキストが切り替わる(Slackでコミュニケーションする、メールをチェックする、Webページを読むなど)可能性が高くなり、1 分の再ビルド時間が数分の作業中断になることもあります。
しかし、インクリメンタルビルドは Bazel のリモートキャッシュで高速化するのが難しいです。なぜなら、入力とそれによる出力のほとんどはローカルで行われる編集に固有だからです。
私たちは ”Bazel と Xcodebuild のインクリメンタルビルド性能は変わらない” という仮説を立て、両方のビルド時間を調査してみました。LINE iOSをリモートキャッシュを切った状態で次の3ケースで数回計測し、ビルド時間の平均値を出しました。
- メインモジュールの一行だけ変更した場合
- 2つのモジュールにある4つのファイルを編集し、1つのファイルは消した場合
- ブランチ切り替えの大きな変更があった場合
結果、Bazel のインクリメンタルビルドは Xcodebuildとそこまで変わらないことが分かりました。むしろ、変更が少ない場合は BazelよりXcodebuild の方が高速という結果が出ました。
そして両方のビルドシステムから出力されたビルドレポートを詳しく調べたところ、Bazel が Xcodebuild より多くのモジュールをリビルドしていることが判明しました。
Bazelは依存関係グラフ上の間接依存関係までリビルド対象にしていました。
仮説の通り、ローカル環境の場合は Bazelの効果が CIビルドほど大きくなかったです。
メンテコストの増大
ローカル環境でのビルド速度に大きな改善はなかったとしても、CIビルド時間の改善だけで Bazelへの移行は十分な成果でした。
ビルドの配布時間の改善によってQA期間のターンアラウンドタイムも短縮でき、開発者が出す Pull Request に対しても、ビルドとテストにかかる時間とボトルネックが改善されたのでDXも改善できました。
しかし、Bazel の運用中にいくつか予想外の問題に直面しました。
Xcodebuildの維持も必要
Bazel は Appleによってメンテナンスされていないサードパーティツールです。
Appleから Xcodeビルドシステムに新しい機能を追加するなどの更新が行ったら、Bazel はオープンソースコミュニティによってアップデートされる必要があります。
しかし、そのアップデートはいつも早い訳ではありません。例えば M1 Macサポートの場合、M1 Macがリリースされたのは 2020年11月ですが、BazelでM1 Macが完全にサポートされたのは1年後の2022年1月でした。
Xcodebuildを維持しなくてもいい iOSプロダクト開発なら Bazelが良い選択肢になる場合もありますが、Xcodeのバージョンアップされるたびに追加された新機能をすぐプロダクト開発に適用したい場合は Bazel と Xcodebuild 両方のビルドシステムを維持するしかありません。
多くのハックが必要
Bazel が Appleの仕様に対応できてないところはハックが必要となります。
LINE iOSでは下記の場合に独自のルールセットを開発したり、Patch や Workaround を適用してきました。
- Obj-C、SwiftのMixed Source Module対応
- Xcode または OS Xの新バージョンに対応
- M1 Mac 上のビルドに対応
- xcarchiveファイルに bundling対応
- xcframeworks のインポートに対応
- 実機でテストターゲットを実行する対応
Bazel側に Patchを入れてカスタマイズしたり、とりあえず動かせるための Workaround を適用する事がよくありました。
Debugging体験が悪い
また、XcodeのDebug機能に不具合が発生しました。
Bazel はビルドツールです。開発者がソースコードを編集してデバッグする操作は Xcode上で行います。そのためには Bazel と Xcodeの統合作業が必須となります。
しかし、Bazel を Xcodeと統合することでいくつかの不具合が発生しました。
- 一部のBreakpointが動作しない
- Auto-complete が機能しないことがある
- Jump to definitions が機能しないことがある
- ‘po’ のようなLLDBが動作しないことがある
LLDB issue
DXにとっては Debug機能に不具合があることは大きな問題でした。
Bazel を XcodeにインテグレーションするにはLLDB設定をオーバーライドする必要があります。
しかし、LLDB設定をカスタマイズすることで一部の Breakpoint が使えなかったり、逆にBazelビルドターゲットでは Breakpoint が正常に動いてるのに Xcodebuildでは動作しない問題が発生しました。
※ LLDB設定のオーバーライド設定
※ Breakpointの不具合
Indexing issue
Xcodeで Auto-complete や Jump to definitions などの機能のために Xcode はIndexing処理を行い、生成した Indexファイルを単一のインデックスストアに出力します。
Bazel環境では BazelのIndex Store と XcodeのIndex Storeを適切に連携して Xcode が検索に使用するパスとインデックスに含まれるファイルパスが一致するようにする設定が必要です。
しかし、前に述べたように Bazel は正確さを確保するためにビルド成果物をサンドボックスに保管します。
これは、各モジュールが独自のインデックスストアを生成するため、Xcode の単一のインデックスストアにインポートするとインデックスファイルが重複する可能性があることを意味します。
Xcode との統合の一環として各モジュールのインデックスストアをXcodeの単一のインデックスストアに移行したら下記のような不具合が発生しました。
• ソースコードの一部に色が付かない
• コードに赤い下線が表示されてるのに、ビルドは成功する
• ⌘ + click すると、「?」とアラートが表示される
• Code suggestionsにタイプが <<error type>>
と表示される
※ Xcodeの Code suggestions機能の不具合
学習コストが高い
Bazelのバージョン1.0 がリリースされたのは 2019年10月です。歴史が短い分、学習資料や導入事例はまだ多くはありません。
Bazelで使われる Starlark という言語の学習コストや、システムのデザインに関するイシューは別としても、開発者が新たなビルドシステムについて理解ができてないままビルド構成を編集し、正しいビルド設定を維持できないことは大きな課題でした。
そのため LINE では社内勉強会の開催やドキュメントの整備が行われましたが、それでも開発者からの質問と問い合わせの件数には変化がなかったです。
ライブラリの管理が大変
ライブラリ管理が難しいことは Bazelの初期バージョンからあったデザインイシューで、設計上の問題が背景となります。
Bazel はライブラリの管理を WORKSPACE というファイル上で行いますが、間接依存関係であるライブラリ側の依存関係までは評価しないので各依存ライブラリのバージョンが予測しにくいです。
Bazelは依存関係グラフを自動解決するリゾルバーを持ちません。すなわち、依存の依存やリビジョンなどを開発者が手動で宣言する必要があります。
また、ソースファイルから依存関係がどこにあるかを自動的に把握してくれるなどの機能も提供せず、むしろコードから依存関係情報を厳しく分けようとしています。なので、Bazel ではHeaderファイルを始めとして直接依存関係と間接依存関係まですべてを宣言する必要があります。
※ XcodeGen と Bazel の依存関係宣言
上の2つは同じビルドターゲットの構成を左が Xcodebuildを維持するための XcodeGen 設定、右が Bazel 側の設定になります。四角で囲った部分が依存関係を宣言する所ですが、Bazelの方は間接依存関係まで宣言しています。
「明確な依存関係を持つ」と言う特徴はメリットにもなりますが、Xcodebuild と Bazel 両方のビルドシステムを維持する際に依存関係の宣言をシンクすることが難しくなります。
キャッシュの管理が大変
Bazelのリモートキャッシュはビルド速度を極端に改善しますが、キャッシュを管理するツールは特に提供していません。
キャッシュになる出力ファイルはハッシュで構成されるため、特定モジュールのキャッシュだけクリーンすることは難しいし、もしキャッシュポイズニングが発生した場合はそれを簡単に検知する方法やデバッグする手段がありません。
キャッシュサーバーの管理コストとサーバーのストレージ費用も考慮する必要が出てきます。
また、LINE社の場合はエンジニアたちが基本リモートワークで働いていて、業務する国や地域がぞれぞれなのでインターネット環境も違い、リモートキャッシュの体験も勤務地によって変わります。
脱Bazelとビルド環境標準化へ
LINE iOS は Bazel によってCIビルドの高速化という利点を享受しましたが、その一方でメンテナンスコストの増加とDXの低下も経験しました。
特に Xcodeとの統合がうまくいかないことで発生するDXに関する問題は深刻な問題でした。
私達はDXを最優先にし、Bazelから得ようとした「速さ」を別の方法で手に入れることに決定しました。リモートキャッシュシステムは別の方法を模索し、一般的なDXを変えずに改善する道を選びました。
現在は Swift Package を中心とした依存関係を管理するなど、Xcodeの標準機能を使ったビルド環境の構築を進めています。
詳しくは iOSDC 2023 でチームの @giginet がトークする予定です。Swift Packageを使った巨大な依存グラフのキャッシュ戦略 by giginet
振り返り
LINEのような大きなプロジェクトでビルドシステムを移行するのは多くの努力と時間が必要です。しかし、私たちはビルドとテスト時間を短縮することに集中して、DXが変わることを過小評価したようです。
BazelをDefaultビルドシステムとして採用する前に次のことに気をつけたらと思いました。
- DXに大きな変化がある時は意思決定にもっと慎重にし、開発者の積極的なフィードバックを誘導する。
- 既存のビルドシステムとBazelを同時運用するならBazel側のビルド設定ファイルである BUILDファイルを自動生成する手段を用意する。
- 依存関係の管理やDebugging機能の正常動作のためにモノレポに変更することを考慮する。
- リモートキャッシュサーバーの管理とキャッシュに関するトラブルに注意する。
- ビルドトラブルを検出するためにビルドのメトリックを細かく測定する。
- モジュールの依存関係を明確に把握する。
まとめ
LINEはビルドシステムをXcodebuildに戻しましたが、LinkedIn、Lyft、Pinterest、Tinder、X(旧Twitter)、Spotify など多くの企業がアプリ開発に Bazel を採用しているそうです。
Bazelの開発者コミュニティは日々拡張していて成功的な導入事例も増えています。
当時は問題だったXcodeとの統合イシューも現在は rules_xcodeproj という新たなルールセットによって解決されたり、翻訳された公式のドキュメントページが公開されるなど、Bazel は現在も進化し続けています。
ただし、大規模プロジェクトこそ Bazel を運用するビルドシステムを所有するチーム、または担当者が必須と言われています。
その担当チームは単純にBazelのアップデートをモニタリングするだけではなくBazelの発展に貢献する姿勢を持ち、Userの立場になった開発者と円滑なコミュニケーションを取りながら開発者がどこで困っているかもモニタリングする必要があると思います。