本文へスキップ

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 のフックをまたいで待つ

待つ場所

裏に回すだけなら & を付ければ済みます。 結果を使う場所の直前で終了を待たなければ、未完了のまま次へ進みます。

ツールのインストールと lint の、開始位置と待機位置

ツールのインストールは、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 つをまとめて始め、使う場所で待ちます。

同期は 174 秒、非同期は 126 秒ほどになった実行時間の内訳

短縮の大きさは、インストールするツールの重さで決まります。 Xcode Cloud のイメージにすでに入っているツールなら、brew install は 1 秒ほどで終わるので、短縮はほとんどありません。

まとめ

ツールのインストールや実行は、結果を使う場所の直前で待つようにすれば、Xcode Cloud でもビルドと並列に走らせられます。 aws CLI を含む 4 つのツールのインストールに 55 秒ほどかかっていたのが、ほぼ 0 秒になり、実行時間は 174 秒ほどから 126 秒ほどになりました。 CI でインストールや実行を待っている処理があれば、よければ試してみてください。

参考リンク

この記事をシェア