irgaly/xcode-cache は、XcodeのDerivedDataとSwiftPMのパッケージをGitHub Actionsのキャッシュに保存するアクションである。コミットごとにキャッシュを保存し、次のビルドでは直前のキャッシュを復元して、変更のあったファイルだけをコンパイルする。
- uses: actions/checkout@v5
- uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-
- name: Test
run: xcodebuild test -project [プロジェクト名].xcodeproj -scheme [スキーム名] -destination 'platform=macOS,arch=arm64'
対象はmacOSのランナーだけである。実行例は、macos-26ランナー(Xcode 26.6)とirgaly/xcode-cache@v1(v1.9.2)のもの。
保存されるもの
アクションは、次の3つを保存する。
| 対象 | 既定の場所 | キャッシュのキー |
|---|---|---|
| DerivedData | ~/Library/Developer/Xcode/DerivedData | keyで指定した値 |
| SourcePackages | ~/Library/Developer/Xcode/DerivedData/[アプリ名]-[ID]/SourcePackages | Package.resolvedのハッシュ値から自動で作られる |
| ソースファイルのmtime | DerivedData/xcode-cache-mtime.json | DerivedDataに含まれる |
SourcePackagesのキャッシュは、DerivedDataとは別のキャッシュとして保存される。キーはirgaly/xcode-cache-sourcepackages-[ハッシュ値]の形式で、Package.resolvedの内容から作られる。
1回目のジョブでは、ジョブの最後に2つのキャッシュが保存された。サイズは、DerivedDataが約60MB、SourcePackagesが約27MBである。
$ gh cache list
xcode-cache-deriveddata-CI-[コミットのSHA] 59.59 MiB
irgaly/xcode-cache-sourcepackages-[ハッシュ値] 27.22 MiB
増分ビルドが効く
Yamsに依存する5つのSwiftファイルとテストの小さなプロジェクトで、次の順に実行した。
- キャッシュがない状態でテストを実行する
Calc1.swiftだけを変更してコミットし、テストを実行する
1回目はキャッシュがないので、すべてのファイルをコンパイルし、パッケージを取得する。ログには、次のとおり出力される。
DerivedData cache not found
There are no SourcePackages directory in DerivedData, skip restoring SourcePackages
Skipped restoring mtime because of DerivedData is not restored
2回目は、keyのSHAが1回目と違うので完全一致しないが、restore-keysの前方一致で1回目のキャッシュが復元される。
Cache hit for restore-key: xcode-cache-deriveddata-CI-[1回目のコミットのSHA]
Cache hit for: irgaly/xcode-cache-sourcepackages-[ハッシュ値]
Restored 5 file's mtimes.
xcodebuildのログで確認した結果は、次のとおりである。SwiftCompileは、ログのSwiftCompileで始まる行数である。
| 実行 | SwiftCompile | パッケージの取得 | xcodebuild testの所要時間 |
|---|---|---|---|
| 1回目(キャッシュなし) | 39行 | する | 26秒 |
2回目(Calc1.swiftを変更) | 2行 | しない | 14秒 |
2回目にコンパイルされたファイルは、変更したCalc1.swiftだけだった。実行例のプロジェクトは小さいので、短縮された時間は十数秒である。ファイルやターゲットが増えると、コンパイルを省略した分だけ差が広がる。
ソースファイルのmtimeが復元される
Xcodeは、ソースファイルの更新時刻(mtime)をナノ秒の精度でDerivedDataに記録する。CIではジョブごとにcheckoutし直すので、中身が同じファイルでもmtimeが変わる。DerivedDataを復元しても、Xcodeはすべてのファイルを変更されたとみなして、再コンパイルする。
irgaly/xcode-cacheは、ジョブの最後にソースファイルのmtimeとSHA-256をxcode-cache-mtime.jsonに保存する。復元時に、SHA-256が同じファイルのmtimeを元に戻す。内容が変わったファイルのmtimeは戻されない。
[
{
"path": "Sources/Calc1.swift",
"time": "1694751978.570357000",
"sha256": "[SHA-256]"
}
]
比較のために、actions/cacheでDerivedDataだけを保存するワークフローも実行した。同じ構成のプロジェクトで、キャッシュを保存した後にCalc3.swiftだけを変更したところ、変更していないCalc1.swiftからCalc5.swiftの5ファイルとテストのファイルもコンパイルされた。
- uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: plain-deriveddata-${{ github.sha }}
restore-keys: plain-deriveddata-
| 方法 | 変更したファイル | コンパイルされたファイル |
|---|---|---|
| irgaly/xcode-cache | Calc1.swift | Calc1.swiftのみ |
actions/cache | Calc3.swift | Calc1.swiftからCalc5.swiftとテストのファイル |
DerivedDataをキャッシュする目的がコンパイルの省略であれば、mtimeの復元が必要である。実行例の小さなプロジェクトでは、actions/cacheとの所要時間の差より、再コンパイルされるファイルの差が明確だった。
keyにgithub.shaを使う
DerivedDataは、増分ビルドのための状態を持つ。keyにはコミットごとに変わるgithub.shaを入れ、restore-keysには共通の接頭辞を指定する。
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-
コミットごとに新しいキャッシュが保存され、次のジョブにはrestore-keysで直近のキャッシュが復元される。
同じコミットのジョブを再実行すると、keyが完全一致する。この場合は、Cache hit for:と表示され、保存の処理はスキップされる。
Cache hit for: xcode-cache-deriveddata-CI-[コミットのSHA]
Restored 6 file's mtimes.
Skipped storing DerivedData
再実行したジョブでは、SwiftCompileが0行で、パッケージの取得もなかった。
Package.resolvedを変更したとき
SourcePackagesのキーには、Package.resolvedのハッシュ値が使われる。依存パッケージのバージョンを変更してPackage.resolvedが変わると、キーが変わる。
Package.resolvedのYamsのバージョンを6.2.2から6.0.0に変更したコミットでは、次のログになった。
Cache hit for restore-key: xcode-cache-deriveddata-CI-[直前のコミットのSHA]
SourcePackages cache not found
Restored 6 file's mtimes.
DerivedDataは復元されたが、SourcePackagesは見つからず、パッケージを再取得した。依存パッケージが変わったので、SwiftCompileも39行に戻り、全ファイルをコンパイルした。
Fetching from https://github.com/jpsim/Yams
Creating working copy of package ‘Yams’
Checking out 6.0.0 of package ‘Yams’
新しいPackage.resolvedに対応するSourcePackagesのキャッシュが、ジョブの最後に追加で保存される。古いキーのSourcePackagesのキャッシュは残る。
参考: 【xcodebuild】SwiftPMの依存関係の解決だけを先に実行する (-resolvePackageDependencies)
使い終わったキャッシュを削除する
github.shaをkeyに含めると、コミットごとにDerivedDataのキャッシュが増える。GitHub Actionsのキャッシュには容量の上限があり、上限を超えると、最後に使われた日時が古いキャッシュから削除される。
delete-used-deriveddata-cache: trueを指定すると、ジョブが成功したときに、restore-keysでヒットして復元に使った、同じブランチの古いDerivedDataのキャッシュを削除する。keyが完全一致したジョブ(同じコミットの再実行)では、削除されない。キャッシュの削除にはAPIを使うので、ジョブにactions: writeの権限が必要である。
permissions:
contents: read
actions: write
steps:
- uses: actions/checkout@v5
- uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-
delete-used-deriveddata-cache: true
3つのキャッシュがある状態で実行すると、復元に使った直近のキャッシュだけが削除され、新しいコミットのキャッシュが追加された。復元に使わなかった古いキャッシュは、そのまま残った。
[ジョブの実行前]
xcode-cache-deriveddata-CI-[SHA 1]
xcode-cache-deriveddata-CI-[SHA 2] ← restore-keysで復元に使う
irgaly/xcode-cache-sourcepackages-[ハッシュ値]
[ジョブの実行後]
xcode-cache-deriveddata-CI-[SHA 1]
xcode-cache-deriveddata-CI-[SHA 3] ← 新しく保存される
irgaly/xcode-cache-sourcepackages-[ハッシュ値]
復元の結果を後続のステップで使う
アクションは、復元の結果をステップの出力に設定する。ステップにidを付けると、後続のステップで参照できる。
- uses: irgaly/xcode-cache@v1
id: xcode-cache
with:
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-
- run: echo "restored=${{ steps.xcode-cache.outputs.restored }} swiftpm=${{ steps.xcode-cache.outputs.swiftpm-restored }}"
| 出力 | 値 |
|---|---|
restored | DerivedDataが復元されたときはtrue(restore-keysのヒットを含む) |
restored-key | ヒットしたDerivedDataのキャッシュのキー |
swiftpm-restored | SourcePackagesが復元されたときはtrue |
swiftpm-restored-key | ヒットしたSourcePackagesのキャッシュのキー |
保存先を指定する
-derivedDataPathや-clonedSourcePackagesDirPathでビルドの保存先を変えているときは、アクションにも同じ場所を指定する。
- uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-
deriveddata-directory: DerivedData
sourcepackages-directory: SourcePackages
- name: Test
run: xcodebuild test -project [プロジェクト名].xcodeproj -scheme [スキーム名] -destination 'platform=macOS,arch=arm64' -derivedDataPath DerivedData -clonedSourcePackagesDirPath SourcePackages
ソースファイルのmtimeを保存する対象は、既定で**/*.swift、**/*.xcassets、**/*.plistなど、Xcodeのビルドの入力になる拡張子のファイルである。ほかの拡張子のファイルも対象にするには、restore-mtime-targetsにグロブパターンを指定する。
restore-mtime-targets: |
[フォルダ名]/**/*
どちらの方法を選ぶか
| やりたいこと | 方法 |
|---|---|
| 変更したファイルだけをコンパイルする | keyにgithub.shaを、restore-keysに共通の接頭辞を指定する |
| パッケージの取得を省略する | Package.resolvedをコミットする(SourcePackagesのキーに使われる) |
| キャッシュの増加を抑える | delete-used-deriveddata-cache: trueとactions: write |
| 保存先を変えたビルドで使う | deriveddata-directoryとsourcepackages-directory |
DerivedDataのキャッシュで増分ビルドを効かせるには、ソースファイルのmtimeの復元が必要である。actions/cacheだけでは、checkoutでmtimeが変わるので、ファイルの内容が同じでも再コンパイルされた。
参考: 【GitHub Actions,Xcode】CIで使うXcodeのバージョンを固定する (xcode-select)
