ローカルでは正常にビルドできるプロジェクトをクラウドMacの無人タスクへ移行したとき、最も見落とされやすいのはコンパイラオプションではなく、Build PhasesにあるRun Scriptです。スクリプトがリポジトリ外の設定を読み込んだり、ソースディレクトリへ書き込んだり、出力が宣言されていないために毎回実行されたりすることがあります。単発の実行では問題が表面化しなくても、並列タスクではファイルを上書きし合い、差分ビルドも次第に意味を失います。この種の障害に対処するには、まず各スクリプトのファイル境界を明示し、その後でサンドボックスを有効にして検証します。先にリトライ回数を増やすべきではありません。
再現可能な監査ベースラインを作る
最初に、プロジェクト、Scheme、ビルド構成、Derived Dataディレクトリを固定します。監査中は普段のディレクトリを使い回さないでください。古い生成物が残っていると、本来欠けている生成処理が正常に動いているように見えることがあります。
set -euo pipefail
ROOT="$PWD"
DERIVED="$ROOT/.audit-derived"
rm -rf "$DERIVED"
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED" \
clean build | tee "$ROOT/audit-clean.log"
Workspaceを使用している場合は、-projectを-workspaceに置き換えます。最初のビルドでは処理全体が完走することを確認します。その後、ソースを変更せずにcleanなしでもう一度ビルドしてください。Run Scriptの実行回数、合計所要時間、出力ファイルの更新時刻を記録します。この2つのログが、それぞれクリーンビルドと差分ビルドのベースラインになります。
監査の目的は、すべてのスクリプトをスキップさせることではありません。入力が変わったとき、出力が存在しないとき、または明示的な実行条件が要求するときに限って各スクリプトが動く状態を作ることです。
すべてのRun Scriptフェーズを棚卸しする
まずプロジェクトファイルからスクリプトを特定し、次にXcodeで所属するTarget、コンパイル前後のどちらに配置されているか、依存関係解析が有効かを確認します。最初の絞り込みにはテキスト検索が適しています。
grep -nE "PBXShellScriptBuildPhase|shellScript =|inputPaths =|outputPaths =" \
App.xcodeproj/project.pbxproj
各フェーズについて表を作成し、スクリプト名だけで済ませないでください。名前が単に「Run Script」となっていることも多く、障害調査の手掛かりにはなりません。
| 確認項目 | 確認すべき内容 |
|---|---|
| 実行条件 | 毎回実行されるのか、依存関係が変化した場合だけ実行されるのか |
| 入力 | どのソース、設定、ツール、ファイルリストを読み込むのか |
| 出力 | 生成ファイル、レポート、完了マーカーをどこへ書き込むのか |
| 副作用 | ソース、グローバル設定、共有キャッシュを変更するか |
| 並行性 | 2つのタスクが同時に実行されたとき、同じパスへ書き込むか |
| 失敗時の動作 | サブコマンドが失敗した直後に非ゼロの終了ステータスを返すか |
スクリプトの先頭にはset -euo pipefailを記述することを推奨します。パイプで連結されたコマンドも確認してください。pipefailがなければ、前段のコマンドが失敗しても、末尾のteeが成功することで失敗が隠れる場合があります。
xcfilelistでファイル境界を宣言する
パスが少ない場合はInput FilesとOutput Filesへ直接入力できます。ファイル数が多い場合は、.xcfilelistを使う方が確認しやすくなります。ユーザーディレクトリをハードコードせず、できるだけ$(SRCROOT)や$(DERIVED_FILE_DIR)などのビルド変数を基準にパスを指定してください。
たとえば、YAML設定からダイジェストを生成するフェーズでは、次の入力リストを使用できます。
$(SRCROOT)/Config/app.yml
$(SRCROOT)/Scripts/generate-config.sh
出力リストには、スクリプトが実際に生成するファイルだけを宣言します。
$(DERIVED_FILE_DIR)/Generated/config.sha256
対応するスクリプトでは、まず一時ファイルを出力先ディレクトリへ書き込み、その後で最終ファイルをアトミックに置き換えます。これにより、並行処理が未完成のファイルを読み込むのを防げます。
set -euo pipefail
SOURCE="$SRCROOT/Config/app.yml"
OUTPUT="$DERIVED_FILE_DIR/Generated/config.sha256"
TEMP="$OUTPUT.tmp.$$"
mkdir -p "$(dirname "$OUTPUT")"
shasum -a 256 "$SOURCE" > "$TEMP"
mv "$TEMP" "$OUTPUT"
リポジトリ全体を大まかに入力として宣言したり、ソースのルートディレクトリを出力に指定したりしないでください。範囲を広げればエラーを抑えられることはありますが、どのファイルが変わってもスクリプトが実行されるようになり、本来の依存関係も見えなくなります。生成物は原則としてDerived Dataへ書き込みます。リポジトリへの書き戻しが必要なコード生成処理は別途実行し、バージョン管理で差分を確認してください。
サンドボックスを有効にして拒否ログを確認する
最初の宣言作業が終わったら、コマンドラインからビルド設定を上書きして検証します。最初からすべての構成を変更する必要はありません。
xcodebuild \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-derivedDataPath "$PWD/.audit-derived" \
ENABLE_USER_SCRIPT_SANDBOXING=YES \
build | tee "$PWD/audit-sandbox.log"
sandbox denyが発生した場合は、拒否されたパス、操作の種類、該当フェーズを最初に特定します。宣言されていない設定を読み込んでいるなら入力へ追加し、宣言されていないファイルを作成または変更しているなら出力へ追加します。ツールがシステムのランタイムライブラリを読み込むためだけに、システムディレクトリ全体をリストへ追加する必要は通常ありません。スクリプトが明示的にアクセスするプロジェクトファイル、設定ファイル、独自ツールのパスを重点的に確認してください。
よくある誤りは、サンドボックスをそのまま無効にすることや、ユーザーのホームディレクトリ全体を入力範囲へ追加することです。前者では暗黙の依存関係が残り続け、後者では制御不能な再ビルドが発生します。スクリプトがリポジトリ外のファイルに依存する場合は、そのファイルをタスクの作業ディレクトリへコピーし、検証したうえで明示的な入力として扱います。
差分ビルドと並列実行の検証をCIへ組み込む
修正後は、少なくとも4種類のテストを実行します。空のDerived Dataを使ったクリーンビルド、ソースを変更しない2回目のビルド、宣言済みの入力を1つだけ変更したビルド、そして独立した2つのDerived Dataディレクトリを使う並列ビルドです。並列タスクではソースの読み取り専用コピーを共有できますが、出力ディレクトリは共有してはいけません。
検証では、次の順序で確認します。
- クリーンビルドで、必要な生成物をゼロからすべて作成できる。
- 2回目のビルドでは、既存の安定した出力を持つスクリプトが無条件に実行されない。
- 宣言済みの入力を変更すると、対応するフェーズが再実行される。
- 無関係なファイルを変更しても、そのフェーズは実行されない。
- 2つの並列タスクが、同じ一時ファイルやレポートを上書きしない。
- スクリプトが失敗すると、
xcodebuildが非ゼロの終了ステータスを返す。 - ログにトークン、秘密鍵の内容、環境変数全体が出力されない。
最後に、チームが実際に使用するビルド構成へサンドボックス設定を反映し、定期的に実行するクリーンビルドのタスクも残します。差分ビルドは速度を担保し、クリーンビルドは依存関係の宣言漏れを検出します。両方に合格して初めて、Run Scriptに再現可能な実行境界があると判断できます。
よくある質問
Run Scriptが毎回のビルドで実行されるのはなぜですか?
出力ファイルが宣言されていないか、依存関係解析に基づく実行が無効になっている場合があります。安定した入出力を宣言すると、Xcodeが再実行の要否を判断できます。
サンドボックスのdenyログはどう直しますか?
拒否されたパスと読み書きの種類をログで確認し、必要なパスをInput Files、Output Files、またはxcfilelistへ追加します。サンドボックスを無効にして回避しないことが重要です。
差分ビルドが正しいことをどう確認しますか?
同じビルドを連続実行し、2回目にフェーズが省略されることを確認します。その後、宣言済み入力を一つ変更し、スクリプトと出力だけが更新されるか検証します。
次回のiOSビルドをMiniDebug M4で実行しましょう。
M4、16GB RAM、256GB SSDを標準搭載。日単位、週単位、月単位、四半期単位でレンタルでき、シンガポール、東京、日本、ソウル、韓国、香港、米国東部の5つのノードから選択できます。実際の利用可能状況はコンソールのリアルタイム表示をご確認ください。