SwiftPMのCI向けキャッシュを考える

## はじめに

Firebase iOS SDK も Swift Package Manager (SwiftPM) サポートがβリリースされたので、CocoaPods や Carthage を剥がして SwiftPM に集約していきたいですよね。

そこで重要になるのが CI でのキャッシュです。Firebase iOS SDK は非常に大きなパッケージで、インストールやビルドに多くの時間を費やしてしまいます。なので、できる限りキャッシュしてCIのビルド時間を短縮することを検討しました。

結論としては「現時点でビルド時間の大幅な短縮には至らない」となったのですが、ここに至るまでに調べたり試した内容をまとめます。

## 従来のパッケージマネージャーの場合
まずは SwiftPM 以前のパッケージマネージャーである CocoaPods と Carthage についてのキャッシュについて要点を整理します。

### CocoaPods
- 依存パッケージ変更の検知には **Podfile.lock** を使用する
- 依存パッケージのソースが含まれる **Pods** ディレクトリをキャッシュ対象とする
- **Pods** ディレクトリにビルド成果物は含まれないため、CIでは毎回ビルドし直す必要がある
    - ただし、非公式のプラグイン [leavez/cocoapods-binary](https://github.com/leavez/cocoapods-binary) を使用することでビルド済バイナリを利用できる

### Carthage
- 依存パッケージ変更の検知には **Cartfile.resolved** を使用する
- 依存パッケージのソースと、それをビルドした Framework が含まれる **Carthage** ディレクトリをキャッシュ対象とする
- ビルドした Framework をキャッシュできるため、CIでのビルド時間を大幅に短縮できる

## SwiftPM の場合

### 🙆‍♂️ 依存パッケージの変更を検知するには
Xcode の GUI から SwiftPM によりパッケージをインストールした場合、そのパッケージ情報は以下のパスにある **Package.resolved** ファイルに出力されます。

> {your_app}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

これを Git リポジトリで管理するようにし、依存パッケージの変更検知に使用します。

### 🙆‍♂️ 依存パッケージのソースをキャッシュするには
依存パッケージのソースは **DerivedData** ディレクトリ配下に次のような形で配置されます。

- ~/Library/Developer/Xcode/DerivedData
    - {プロジェクト毎のディレクトリ}
        - **SourcePackages**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/f789016d-6551-5325-a2b2-0775d2cde750.png" width=300>

この **SourcePackages** ディレクトリをキャッシュできれば良いのですが、親のディレクトリ名は動的に毎回違うものが生成されてしまいますので、これではキャッシュとして機能しません。

実は Xcode のビルドに使われるコマンド **xcodebuild** には、この SourcePackages ディレクトリのパスを指定する **-clonedSourcePackagesDirPath** オプションがありますので、これで任意のパスを指定することができます。これでディレクトリを固定することができるので、CIでキャッシュすることができるようになりました。その結果、依存パッケージを Clone してくる1〜2分だけビルド時間を短縮することができました。

なお、**-clonedSourcePackagesDirPath** オプションを `xcodebuild --help` で確認すると次のように記述されています。

> specifies the directory to which remote source packages are fetch or expected to be found

ビルドに fastlane を利用している場合、Xcode のビルドに関する公式 lane には **cloned_source_packages_path** オプションがあるため、こちらを利用するようにしてください。

### 🙅‍♂️ 依存パッケージのビルド済みバイナリをキャッシュするには

さて、ビルド時間に大きく影響を与えるビルド済みバイナリのキャッシュについてです。

これも **DerivedData** 配下に出力され、おそらくその中の **Build** ディレクトリに入っていそうです。ただ、先ほどの -clonedSourcePackagesDirPath オプションのようにビルド済みバイナリの出力パスを変えるようなオプションは xcodebuild コマンドには見当たりません。

代わりに DerivedData ディレクトリそのもののパスを指定できる **-derivedDataPath** オプション（以下、ヘルプの説明）が見つかったので、これを指定して DerivedData ディレクトリをまるっとキャッシュするとどうでしょうか。

> specifies the directory where build products and other derived data will go

結論としては、これではキャッシュとして機能しませんでした😭。そう単純にはいかないようです。

なんとかキャッシュできるように調べていると、Bitrise 公式のブログにある [60% faster builds: force Xcode to use caching on Bitrise!](
https://blog.bitrise.io/60-percent-faster-builds-force-xcode-use-caching) という記事にたどり着きました。ここにキャッシュとして機能しない理由は、次のように説明されています。

> When you clone a repository on Bitrise for a build, all the files' modification times will be set to the current time (the time of git clone), so all your files will be considered changed for every new build, even if most files' content didn't change at all between builds.
> (翻訳) Bitrise でリポジトリをクローンしてビルドすると、すべてのファイルの変更時刻が現在の時刻 (git clone の時刻) に設定されるので、ビルドの間にほとんどのファイルの内容がまったく変更されなかったとしても、新しいビルドのたびにすべてのファイルが変更されたとみなされます。

さらに、この問題を回避して DerivedData をキャッシュするための [steps-recursive-touch](https://github.com/bitrise-steplib/steps-recursive-touch/issues) ステップを追加したという記述があり、早速このステップのリポジトリを確認すると2019年12月14日に廃止されており、利用できなくなっていました。

このリポジトリの Issue [bitrise-steplib/steps-recursive-touch#1](https://github.com/bitrise-steplib/steps-recursive-touch/issues/1) で廃止理由が次のように述べられています。

> This step was deprecated because the new build system (the Xcode 10.2 final release if I recall correctly) broke this solution completely.

どうやら Xcode 10 のビルドシステムの変更により、このステップで提供していたソリューションも動作しなくなったようです。その後、これの代替となるものはなさそうなので、現時点で DerivedData をキャッシュとして利用することは難しそうです。

## 今後
SwiftPM ではビルド済みのバイナリを **XCFramework** というパッケージで配布する機能があります（この機能は2019年のWWDCで発表されました）。

Firebase iOS SDK も SwiftPM で XCFramework を提供するプロポーザル [firebase/firebase-ios-sdk/#6564](https://github.com/firebase/firebase-ios-sdk/issues/6564) が挙がっています。これが実現されれば、パッケージのキャッシュのみでビルド時間を大幅に削減できることになります！

(2021/2/19追記) FirebaseAnalytics のみビルド済の XCFramework で提供されるようになりました。SourcePackagesディレクトリ内の artifacts に配置されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/4151d03a-3424-518f-906b-065a034986bf.png" width=400>

## 結論

もうしばらくの間、Firebase iOS SDK などのビルド時間の長い大きなパッケージを含む場合は、ビルド時間を優先して Carthage を使い続けようと思います。

もし、他の良いソリューションがあればご教授ください🙏

## 参考文献

- CocoaPods でビルド済みバイナリを使用する
    - [CocoaPods / Guides / Pre-compiling dependencies](https://guides.cocoapods.org/plugins/pre-compiling-dependencies.html)
    - [Qiita / CocoaPods でビルド済みバイナリを使う](https://qiita.com/usamik26/items/ce037326a62f05ac47c7) by @usamik26
- CI で SwiftPM のキャッシュを利用する
    - [UPTECH, INC. / Swift Package Manager And How To Cache It With CI](https://uptech.team/blog/swift-package-manager)
    - [麦芽を支える技術 / BitriseでSwift Package Manager(SPM)のパッケージをキャッシュする](https://asmz.hatenablog.jp/entry/cache-swift-package-manager-on-bitrise)
- DerivedData をキャッシュする
    - [Bitrise / 60% faster builds: force Xcode to use caching on Bitrise!](https://blog.bitrise.io/60-percent-faster-builds-force-xcode-use-caching)
    - [bitrise-steplib/steps-recursive-touch : Recursive Touch for Cache step](https://github.com/bitrise-steplib/steps-recursive-touch)
- XCFramework について
    - [Apple Developer / WWDC 2019 / Binary Frameworks in Swift](https://developer.apple.com/videos/play/wwdc2019/416/)
    - [Apple Developer / WWDC 2020 / Distribute binary frameworks as Swift packages](https://developer.apple.com/videos/play/wwdc2020/10147)
    - [Apple Developer / Distributing Binary Frameworks as Swift Packages](https://developer.apple.com/documentation/swift_packages/distributing_binary_frameworks_as_swift_packages)