こんにちは。『スタディサプリ』 iOS 開発チームの @manicmaniac です。
今回は、Xcode のビルドフェーズで nohup を使って SwiftLint や LicensePlist などのツールを非同期実行することで、開発中のインクリメンタルビルドの体感時間を大幅に短縮する工夫について書いてみます。
問題:静的解析ツールがビルド時間を圧迫
iOS アプリ開発において、SwiftLint や LicensePlist といったツールは品質管理に欠かせません。しかし、これらのツールを Xcode の Build Phases で同期実行すると、プロジェクトが大きくなるにつれて開発中の頻繁なインクリメンタルビルドの待機時間が無視できないレベルになってきました。
特に以下のような課題がありました:
- SwiftLint の実行に毎回数十秒かかる
- LicensePlist の処理も同様に時間がかかる
- 開発中の頻繁なビルドで待機時間が積み重なる
- ローカル開発だけでなく CI 環境でも課題となっていた
解決策:nohup を使った非同期実行
SwiftLint や LicensePlist の実行はビルドの処理本体とは関係ないので、これらの処理を待たず非同期で実行したいところです。
ところが、Xcode は Build Phase Script の非同期実行をサポートしていないので、やや無理矢理ですが nohup コマンドとシェルのバックグラウンド実行を使ってこれらのツールを非同期実行することにしました。
実装例
SwiftLint の非同期実行
CI 環境については異なるアプローチを取っており、SwiftLint のような静的解析ツールはビルドとは別のジョブで実行するほうが都合が良いので、Xcode の Build Phase Script では実行しないようにしています。これにより CI 環境でも静的解析による時間増加の問題を解決しています。
また、--cache-path を指定することで多少高速化できます。SwiftLint は前回の実行結果をキャッシュファイルに保存し、変更されたファイルのみを解析することで実行時間を短縮します。このキャッシュ機能により、大規模なプロジェクトでも差分解析による高速化が期待できます。
if [ "$CI" != true ]; then
nohup swiftlint --cache-path "$TEMP_DIR/swiftlint" > "${TEMP_DIR}/swiftlint.stdout.log" 2> "${TEMP_DIR}/swiftlint.stderr.log" &
echo $! > "${TEMP_DIR}/swiftlint.pid"
fi
LicensePlist など、他のツールについても同様の仕組みで非同期実行しています。
実行完了を待つ処理
ビルド処理の最後で、バックグラウンドで実行しているツールの完了を待つ仕組みも重要です。
# SwiftLint の完了を待つ
if [ "$CI" != true ]; then
while xargs kill -0 < "${TEMP_DIR}/swiftlint.pid" 2> /dev/null
do
sleep 1
done
cat "${TEMP_DIR}/swiftlint.stdout.log"
cat "${TEMP_DIR}/swiftlint.stderr.log" >&2
rm -f "${TEMP_DIR}/swiftlint.pid"
fi
他のツールについても、各ツールごとに同様の PID ファイルとログファイルを用意して、同じパターンで完了を待機しています。
ポイント解説
まず nohup とバックグラウンド実行(&)の組み合わせがキーポイントです。nohup を使うことでプロセスをターミナルから切り離し、& でバックグラウンド実行することで、Xcode のビルドプロセスが終了した後もツールが動き続けます。
ログ出力については、stdout と stderr を別々のファイルに分けて出力するようにしました。また TEMP_DIR を使うことで、不要になったログファイルのクリーンアップも簡単になります。
プロセス管理のため、echo $! > "${TEMP_DIR}/swiftlint.pid" でプロセス ID をファイルに保存しています。これにより、必要に応じて後からプロセスを制御できるようになります。
実行完了の待機処理では、kill -0 コマンドを使ってプロセスの生存確認を行っています。kill -0 は実際にシグナルを送らずに、指定したプロセス ID が存在するかどうかだけを確認するコマンドです。プロセスが終了していれば非ゼロの終了ステータスを返すため、while ループから抜けてログの出力とクリーンアップが実行されます。
CI 環境については特別な配慮が必要で、if [ "$CI" != true ] という条件分岐を入れています。この条件により、CI 環境ではこれらのスクリプト自体が実行されません。CI では静的解析ツールを別のジョブで実行することで、確実性と並列性の両方を実現しています。
実装前後の比較
Build Phases の構成変更
実装前は SwiftLint と LicensePlist を同期実行していましたが、実装後は以下のように変更しました:
Before(実装前)
- SwiftLint(同期実行)
- LicensePlist(同期実行)
After(実装後)
- Start SwiftLint(非同期実行開始)
- Start LicensePlist(非同期実行開始)
- (他のビルド処理)
- Finish SwiftLint(完了待ち)
- Finish LicensePlist(完了待ち)
結果とメリット
開発中のインクリメンタルビルドの体感時間短縮
この実装により、最も大きな改善はローカル開発での小変更に伴うインクリメンタルビルドが数秒で完了するようになったことです。変更のサイズにもよりますが、基本的には SwiftLint などのツールの実行時間より、ビルド本体の時間の方が長くかかるため、もともと合計 30秒以上かかっていたツール実行の待機時間は解消されました。
開発効率の向上
頻繁なビルド・テストサイクルが快適になったことで、開発者が集中力を維持したままコーディングを継続できるようになりました。特に細かい調整を繰り返すような作業において、待機時間がなくなることの恩恵は大きく感じられます。
品質管理の継続
重要なポイントとして、ツールは引き続きバックグラウンドで実行されているため、品質管理は維持されています。問題があればログファイルで確認できるため、コード品質を犠牲にすることなく開発速度を向上させることができました。
注意点と考慮事項
ツール実行結果の確認
この手法の最大の注意点は、ツールがエラーで失敗してもビルド自体は成功してしまうことです。従来の同期実行では、静的解析ツールがエラーで終了するとビルドも失敗していましたが、非同期実行ではビルドは成功し、後でログを確認してエラーに気づくという流れになります。エラーの見落としを防ぐため、定期的なログチェックや適切な監視の仕組みが重要になります。
並列ビルドでの PID ファイル競合
ロックを実装していないため、Xcode のビルドプロセスが並列で実行される場合、PID ファイルが上書きされる可能性があります。厳密にはレースコンディションが発生しうる状況ですが、実用上は大きな問題にはなりにくいため、実装の単純さを取ることにしました。
まとめ
nohup を使った非同期実行は、やや力技ではあるものの、開発体験を大きく改善する効果的な手法でした。特に大規模なプロジェクトにおいて、開発中の頻繁なインクリメンタルビルドの体感時間短縮は開発者の生産性向上に直結します。
一方で、品質管理ツールの実行結果を適切に監視する仕組みも重要です。この両立を図ることで、快適で安全な開発環境を構築できると考えています。
同様の課題に直面している方の参考になれば幸いです。