Xcode Cloud でツールのインストールや実行を非同期化して CI の実行時間を短縮する
背景
Xcode Cloud のビルドでは、ツールのインストールや実行にも時間がかかります。
ci_post_clone.sh や ci_post_xcodebuild.sh で brew install を実行していると、その間は次の処理が止まります。
ところが、そのツールを使うのはビルドの途中や後処理で、インストールの完了を今すぐ待つ必要はありません。
nohup とバックグラウンド実行で裏に回し、使う場所の直前で待つようにします。
ここでは次の 2 つのやり方を紹介します。
- Build Phases の Run Script で、lint をコンパイルと並列に実行する
ci_post_clone.shでツールのインストールを始め、Xcode Cloud のフックをまたいで待つ
待つ場所
裏に回すだけなら & を付ければ済みます。
結果を使う場所の直前で終了を待たなければ、未完了のまま次へ進みます。
ツールのインストールは、ci_post_clone.sh で始めます。
Xcode Cloud は、このあとパッケージを解決してから xcodebuild を始めます。
パッケージ解決の時間も、インストールの裏に隠せます。
待つ場所は、ツールを使う場所に合わせます。
ci_pre_xcodebuild.sh で使うツールはそこで、Run Script で使うツールはそのフェーズで、ci_post_xcodebuild.sh で使うツールはそこで待ちます。
swift-format の警告は、xcodebuild の実行中に出力しないとビルドの警告として扱われないので、開始と待機を Build Phases の Run Script に置きます。
ツールのインストールをフックをまたいで並列にする
ci_post_clone.sh では、1 本のバックグラウンドプロセスがツールを順番にインストールします。
brew install を同時に複数起動するとロックを取り合うので、並べて走らせはしません。
各ツールの完了は、ファイルで知らせます。
# ci_scripts/ci_post_clone.sh
"${SCRIPT_DIR}/tool_install.sh" start xcodegen swiftgen swiftlint awscli
# ci_scripts/tool_install.sh(抜粋)
STATE_DIR="/tmp/xcode_cloud_tool_install"
install_with_retry() {
local tool="$1" i
for ((i = 1; i <= 3; i++)); do
command -v "$(command_of "${tool}")" >/dev/null 2>&1 && return 0
brew install "${tool}" && return 0
sleep 5
done
return 1
}
case "$1" in
start)
shift
mkdir -p "${STATE_DIR}"
nohup "$0" run "$@" > "${STATE_DIR}/log" 2>&1 < /dev/null &
echo $! > "${STATE_DIR}/pid"
;;
run)
shift
for tool in "$@"; do
if install_with_retry "${tool}"; then
touch "${STATE_DIR}/${tool}.done"
else
touch "${STATE_DIR}/${tool}.failed"
fi
done
;;
wait)
# pid が無ければ、その場で同期インストールする
# ${tool}.done ができるまで待ち、できなければ exit 1 で CI を止める
;;
esac
出力はファイルに向け、標準入力は /dev/null にします。
開始と待機は別のプロセスなので、run が完了を .done と .failed のファイルで知らせ、待機側がそれを見ます。
失敗しても upload の直前まで進んでから落ちるより、ツールを使う場所で止めたほうが原因が分かりやすくなります。
待機は、そのツールを使う場所で呼びます。
# ci_scripts/ci_pre_xcodebuild.sh
"${SCRIPT_DIR}/tool_install.sh" wait xcodegen || exit 1
xcodegen --version
# ci_scripts/ci_post_xcodebuild.sh
"${SCRIPT_DIR}/tool_install.sh" wait awscli || error_exit "AWS CLI インストール失敗"
後処理では、PR にコメントを投稿するスクリプトと、成果物のアップロードが独立しています。
こちらは & と wait で並列にします。
"${SCRIPT_DIR}/post_report.sh" &
REPORT_PID=$!
# ...zip 化とアップロード...
wait "${REPORT_PID}" || echo "post_report.sh が失敗しました" >&2
swift-format を Run Script で並列にする
Run Script を 2 つ置き、同じスクリプトに start と wait を渡します。
1. Start swift-format lint ← scripts/swift_format_lint.sh start
2. Compile Sources
3. Link Binary With Libraries
4. Copy Bundle Resources
5. Wait swift-format lint ← scripts/swift_format_lint.sh wait
case "$1" in
start)
nohup "$0" run > "${OUT_LOG}" 2> "${ERR_LOG}" < /dev/null &
echo $! > "${PID_FILE}"
;;
run)
cd "${SRCROOT}" || exit 0
git ls-files -z -- '*.swift' | xargs -0 xcrun swift-format lint --parallel
;;
wait)
[ -f "${PID_FILE}" ] || exit 0
while kill -0 "$(cat "${PID_FILE}")" 2>/dev/null; do sleep 1; done
cat "${OUT_LOG}"
cat "${ERR_LOG}" >&2
;;
esac
exit 0
wait で cat した file:line:col: warning: ... の行を、Xcode がビルドの警告として扱います。
--strict を付けなければ、指摘があってもビルドは止まりません。
結果
同じワークフローで、xcodegen、swiftgen、swiftlint、aws CLI の 4 つを Homebrew でインストールする構成を、同期と非同期で 2 回ずつ実行しました。
同期は ci_post_clone.sh で 3 つを、ci_post_xcodebuild.sh で aws CLI をその場でインストールします。
非同期は ci_post_clone.sh で 4 つをまとめて始め、使う場所で待ちます。
短縮の大きさは、インストールするツールの重さで決まります。
Xcode Cloud のイメージにすでに入っているツールなら、brew install は 1 秒ほどで終わるので、短縮はほとんどありません。
まとめ
ツールのインストールや実行は、結果を使う場所の直前で待つようにすれば、Xcode Cloud でもビルドと並列に走らせられます。 aws CLI を含む 4 つのツールのインストールに 55 秒ほどかかっていたのが、ほぼ 0 秒になり、実行時間は 174 秒ほどから 126 秒ほどになりました。 CI でインストールや実行を待っている処理があれば、よければ試してみてください。