GitHub Actions: xcode-installで追加したiOSをキャッシュする


GitHub Actionsを使って継続的にUITestを実行する際、古いiOSや特定のiOSで動作確認をしたい場合があります。しかし、GitHub ActionsのmacOS環境にインストールされているXcodeには複数のiOSが用意されていません。[1] 代わりにxcode-installがプリインストールされているため、xcversion simulators --installを用いて任意のiOSをインストールすることができます。

ただ、Simulator用のiOSは圧縮された状態でも5GB前後(展開すると10GBを超えるものも)あるため、xcode-installでインストールするにも10分から20分ほどかかります。また、xcode-installにキャッシュする仕組みはないため、workflow実行のたびにxcode-installでインストールすることになり非効率です。

そこで、今回はGitHub Actions公式のactions/cacheを用いてxcode-installで追加したiOSをキャッシュする方法を検討しました。(なぜか前例が全然出てこなかった)

actions/cacheの仕様

  • 1リポジトリあたり最大で10GBまでキャッシュ可能
  • 10GBを超える場合は古いキャッシュから消されていく
  • 1週間以内に使われなかったキャッシュは随時消されていく
  • actions/upload-artifactとは違い保存と取り出しが高速(恐らく別サーバーのストレージにアップロードするわけではないため)

キャッシュする仕組みの要点

  • Simulator用のiOSの実体はiOS バージョン.simruntimeという名前のディレクトリ
  • 後から追加したsimruntime/Library/Developer/CoreSimulator/Profiles/Runtimesに格納される
    • ここにあるsimruntimeが読み込まれて自動的にiOS Simulatorで使えるようになる
  • simruntimeは単体で10GBを超えるほど大きいためそのままではキャッシュできない
    • zipで圧縮してファイルサイズを小さくしてからキャッシュする

actions/cacheの上限を踏まえると、一つのリポジトリでキャッシュ運用できるiOSは1バージョンだけとなりそうです。特にSwift Package ManagerCocoaPodsなどでライブラリを使っておりそれもキャッシュするとなると上限の10GB以内に複数のiOSをキャッシュすることはできないでしょう。

Workflowの例

Xcode 13.2.1にiOS 14.5をインストールする例を示します。[2]

name: Dispatch Workflow

on:
  workflow_dispatch:

jobs:
  setup-ios-14:
    name: Setup iOS 14.5 to Xcode 13.2.1
    runs-on: macos-11
    timeout-minutes: 30
    env:
      DEVELOPER_DIR: "/Applications/Xcode_13.2.1.app/Contents/Developer"
      RUNTIMES_DIR: "/Library/Developer/CoreSimulator/Profiles/Runtimes"

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # Zipがキャッシュしてあれば取り出す
      - name: Cache simruntime
        id: cache-simruntime
        uses: actions/cache@v2
        with:
          path: ios_14.5_simruntime.zip
          key: ${{ runner.os }}-simruntime

      # キャッシュから取り出したZipを展開
      - name: Unzip simruntime
        if: steps.cache-simruntime.outputs.cache-hit == 'true'
        run: |
          sudo mkdir -p ${RUNTIMES_DIR}
          sudo unzip ios_14.5_simruntime.zip -d ${RUNTIMES_DIR}

      # xcode-installを用いて指定のiOSをインストール
      - name: Install simruntime
        if: steps.cache-simruntime.outputs.cache-hit != 'true'
        run: |
          xcversion simulators --install='iOS 14.5'
          ls -d ${RUNTIMES_DIR}/* | xargs -Ipath du -s -m "path"

      # この段階で追加したiOSが使えるようになる
      - name: List up simulators
        run: |
          xcrun xctrace list devices 2>&1 | \
          grep -v Apple | \
          sed -r "s/Simulator //g"

      # ワークフローのJobが終わる前に指定のパスにZipを用意しておくとキャッシュしてくれる
      - name: Zip simruntime
        if: steps.cache-simruntime.outputs.cache-hit != 'true'
        working-directory: ${{ env.RUNTIMES_DIR }}
        run: |
          ls -1 -d * | head -n 1 | \
          xargs -Ipath zip -r ${{ github.workspace }}/ios_14.5_simruntime.zip "path"

ポイント

  • actions/cache@v2を使う
    • @v3は2GB以上を一度にキャッシュできない不具合がある?[3]
  • 初回のWorkflow実行時間はiOSをインストールするのとZipで圧縮するので非常に時間がかかる
  • 2回目以降はキャッシュから取り出すのとUnzipで展開する時間しかかからないので時短できる
  • キャッシュがあった場合となかった場合でやるべき処理を分岐している
    if: steps.cache-simruntime.outputs.cache-hit == 'true'
    if: steps.cache-simruntime.outputs.cache-hit != 'true'
    
  • xcversion simulators --installを一度もしていない場合
    /Library/Developer/CoreSimulator/Profiles/Runtimesのディレクトリは存在しない
  • /Libraryの下にディレクトリを作ったりUnzipでファイルを展開する場合はsudoが必要

iOSをインストールするJobと使うJobを分ける場合

name: Dispatch Workflow

on:
  workflow_dispatch:

env:
  DEVELOPER_DIR: "/Applications/Xcode_13.2.1.app/Contents/Developer"
  RUNTIMES_DIR: "/Library/Developer/CoreSimulator/Profiles/Runtimes"

jobs:
  # iOSを追加するフェーズ
  setup-ios:
    name: Setup iOS 14.5 to Xcode 13.2.1
    runs-on: macos-11
    timeout-minutes: 40
    strategy:
      fail-fast: false

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Cache simruntime
        id: cache-simruntime
        uses: actions/cache@v2
        with:
          path: ios_14.5_simruntime.zip
          key: ${{ runner.os }}-14.5-simruntime

      - name: Install simruntime
        if: steps.cache-simruntime.outputs.cache-hit != 'true'
        run: |
          xcversion simulators --install='iOS 14.5'
          ls -d ${RUNTIMES_DIR}/* | xargs -Ipath du -s -m "path"

      - name: Zip simruntime
        if: steps.cache-simruntime.outputs.cache-hit != 'true'
        working-directory: ${{ env.RUNTIMES_DIR }}
        run: |
          ls -1 -d * | head -n 1 | \
          xargs -Ipath zip -r -q -7 ${{ github.workspace }}/ios_14.5_simruntime.zip "path"

  # 追加したiOSを利用するフェーズ
  use-installed-ios:
    name: Use installed iOS
    runs-on: macos-11
    needs: [setup-ios]

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Cache simruntime
        id: cache-simruntime
        uses: actions/cache@v2
        with:
          path: ios_14.5_simruntime.zip
          key: ${{ runner.os }}-14.5-simruntime

      # キャッシュが存在していなかったら何もできないので失敗扱い
      - name: Exit if cache does not exist
        if: steps.cache-simruntime.outputs.cache-hit != 'true'
        run: exit 1

      - name: Unzip simruntime
        run: |
          sudo mkdir -p ${RUNTIMES_DIR}
          sudo unzip -q ios_14.5_simruntime.zip -d ${RUNTIMES_DIR}

      - name: Look up simulators
        run: xcrun xctrace list devices 2>&1 | grep -v Apple
脚注
  1. macOS 11.6 info - Installed SDKs ↩︎

  2. Kyome22/cache-ios-simruntime ↩︎

  3. Can't restore cache biger than 2G in actions/cache@v3 ↩︎