エンジニアリング記事

クラウドMacでSwiftコンパイラのクラッシュを切り分ける

クラウドMacでSwiftコンパイラのクラッシュを切り分ける

同じコミットが開発機では正常にビルドできるのに、クラウドMacの継続ビルドでは swift-frontend が異常終了することがあります。再実行すると偶然成功し、キャッシュを消すと今度は別のファイルで失敗する場合もあります。このとき最も避けるべきなのは、ソースの変更、依存関係の更新、全キャッシュの削除を立て続けに行うことです。検証環境を同時に変えると、コンパイラのクラッシュを引き起こした条件まで失われてしまいます。まず障害の種類を確認し、その後、一度に一つの変数だけを変更して範囲を絞り込むのが確実です。

コンパイラのクラッシュかどうかを確認する

CompileSwiftSources の失敗が、そのままコンパイラのクラッシュを意味するわけではありません。構文エラー、型推論の失敗、モジュールの欠落でも、この段階でゼロ以外のステータスが返ります。以下の兆候がある場合は、本記事の手順で調査する価値があります。

  • ログに swift-frontend の異常終了または signal 11 が明記されている。
  • 出力に Stack dump、内部アサーションの失敗、コンパイラのコールスタックが含まれている。
  • ~/Library/Logs/DiagnosticReports/ に、発生時刻と一致する swift-frontend のレポートがある。
  • 一時的なリモートセッションの切断ではなく、同じソースとコマンドで複数回再現できる。

最初にツールチェーンとホストの情報を記録します。ログの末尾10行だけを抜き出してはいけません。

mkdir -p diagnostics
{
  date -u
  sw_vers
  uname -m
  xcode-select -p
  xcodebuild -version
  xcrun swiftc --version
} | tee diagnostics/environment.txt

find "$HOME/Library/Logs/DiagnosticReports" \
  -maxdepth 1 -type f -name 'swift-frontend*' \
  -exec cp {} diagnostics/ \;

クラッシュレポート、完全なビルドコマンド、クラッシュを引き起こしたソースは、必ず同じ再現試行から取得してください。異なる試行の資料を組み合わせると、検証できない誤った手掛かりになりがちです。

隔離ビルドで最初の状態を保全する

再現専用のDerivedDataを用意し、ターミナル出力をすべて保存します。最初からグローバルキャッシュを削除してはいけません。キャッシュが発生条件に関係しているかどうかも、検証すべき変数の一つだからです。

set -o pipefail
rm -rf "$PWD/.diagnostics-derived-data"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.diagnostics-derived-data" \
  -jobs 1 \
  build 2>&1 | tee diagnostics/build-single-job.log

ここで並列数を1に下げるのは、クラッシュが並列コンパイルに依存するかどうかを確認するためだけであり、恒久的な設定を推奨するものではありません。単一ジョブでも安定してクラッシュする場合は、そのログを保存します。障害が消える場合は、ソースとDerivedDataを固定したまま、-jobs 2 と通常利用している並列数を個別にテストしてください。各条件を最低3回繰り返し、成功、通常のコンパイルエラー、コンパイラのクラッシュのどれだったかを記録します。すべてを単に「失敗」と記録してはいけません。

OVPSのクラウドMacで再現する場合は、実際に選択されているXcodeのパスも記録してください。画面に表示されるバージョン名の比較だけでは不十分です。同じメジャーバージョンでも、ビルド番号やSwiftツールチェーンが異なる可能性があります。

単一変数の比較マトリクスを作る

調査は、検証コストが低く、環境への影響が小さい変数から始めます。一度に変更する項目は一つだけにし、試行ごとに新しいログを保存してください。

比較項目 ベースライン 変更値 確認する内容
並列数 -jobs 1 通常の並列数 並列コンパイル時だけ発生するか
DerivedData 隔離ディレクトリ 新しい空のディレクトリ 既存の中間生成物に依存するか
コンパイルモード 現在のプロジェクト設定値 一時的な比較値 インクリメンタルまたはモジュール全体のコンパイル経路に関係するか
最適化レベル Debug設定 プロジェクトで利用可能な比較設定 最適化時だけ発生するか
ツールチェーン 現在固定しているバージョン 検証済みの別バージョン 特定ツールチェーンのリグレッションか

Xcodeの切り替え、依存関係の更新、キャッシュの削除を同時に行ってはいけません。ツールチェーンを切り替えて問題が消えても、ツールチェーンの組み合わせが障害に関係していると分かるだけで、新しいバージョンで修正済みだと直接証明できるわけではありません。失敗したファイル、コンパイルモード、発生回数も同じ記録にまとめてください。

キャッシュによる見かけの変化を見分ける

空のDerivedDataでは成功し、古いディレクトリでは安定して失敗する場合に限り、モジュールキャッシュとビルドデータベースを詳しく調査します。まず問題のあるディレクトリをアーカイブし、その後で対象を限定して削除してください。広範囲をいきなり削除すると、古い生成物と新しい生成物を比較する機会が失われます。

失敗したファイルを最小再現まで縮小する

完全なログから、失敗したSwiftコンパイルタスクを特定します。プロジェクトから切り離して再現できる場合は、関連する宣言を Repro.swift にコピーし、まず型チェックで確認します。

xcrun swiftc -typecheck Repro.swift 2>&1 | tee diagnostics/repro.log

ソースを縮小するときは、依存関係と逆の順序で進めます。最初に無関係なメソッドを削除し、次にプロトコル実装、ジェネリック制約、プロパティラッパーを取り除きます。コードを一つ削るたびに判定スクリプトを実行してください。構文が「複雑に見える」という理由だけで原因を推測してはいけません。コンパイラのクラッシュは、単独では一般的な二つの機能の組み合わせによって発生することがよくあります。

#!/bin/zsh
set -o pipefail

output="$(mktemp)"
xcrun swiftc -typecheck Repro.swift >"$output" 2>&1

if grep -Eiq 'signal 11|segmentation fault|stack dump|swift-frontend.*failed' "$output"; then
  cp "$output" diagnostics/latest-crash.log
  rm -f "$output"
  exit 0
fi

rm -f "$output"
exit 1

このスクリプトでは、「対象のクラッシュが引き続き発生する」状態を成功として扱います。そのため、手動での二分探索やソース縮小ツールと組み合わせやすくなります。縮小するたびに、エラーシグネチャが同じであることも確認してください。内部アサーションの失敗が通常の型エラーに変わった場合は、重要な発生条件を削除してしまったことを意味します。

単一ファイルで再現できない場合は、必要最小限のモジュール境界を残します。具体的には、最小構成のプロジェクト、必要なビルド設定、固定した依存関係のバージョン、実行コマンド1つです。業務データ、アクセス認証情報、無関係なリソースは含めないでください。

再検証できる引き渡しパッケージを作る

最終的な資料は、口頭説明がなくても別のクラウドMacで検証を完了できる形にします。ディレクトリに含めるものは、次の項目だけにすることを推奨します。

  • README.md:想定される現象、実行コマンド、繰り返し回数、実際の結果。
  • environment.txt:システムアーキテクチャ、Xcodeのビルド番号、Swiftのバージョン。
  • Repro.swift または最小構成のプロジェクト:必要なソースだけを残したもの。
  • build.log:省略されていない標準出力と標準エラー出力。
  • クラッシュレポート:実行時刻と一致する診断ファイル。
  • check.sh:対象のクラッシュを再現できたかどうかを終了ステータスで明確に示すスクリプト。

引き渡し前に新しいディレクトリでもう一度実行し、スクリプトが元のプロジェクトの絶対パス、ユーザーディレクトリ、残存するモジュールキャッシュに依存していないことを確認します。特定の並列数でのみ問題が起きる場合は、その並列パラメータと再現率を明記してください。特定のツールチェーンのビルド番号でのみ発生する場合は、正常に通る比較対象のバージョンも記録します。こうして得られるのは、「Swiftが時々クラッシュする」という一言ではなく、再現、比較、継続的な修正に利用できる一連の技術的証拠です。

よくある質問

CompileSwiftSourcesの失敗は常にコンパイラのクラッシュですか?

いいえ。構文エラー、型検査エラー、依存関係の不足でも同じ段階が失敗します。swift-frontendの異常終了、signal、Stack dump、対応する診断レポートを確認してください。

最小再現にはXcodeプロジェクト全体が必要ですか?

必須ではありません。単独のSwiftファイルとswiftcコマンドで安定して再現できれば、それが最適です。モジュール境界やビルド設定が原因の場合だけ縮小したプロジェクトを残します。

OVPS クラウドMac

次回のビルドを専有物理ノードで実行する

3種類の Apple Silicon 構成と、販売中の6つのノードから選択できます。実際の利用可能状況は、コンソールにリアルタイムで表示される情報をご確認ください。

構成を選んで注文する