はじめに
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 を使用することでビルド済バイナリを利用できる
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
- {プロジェクト毎のディレクトリ}
この 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! という記事にたどり着きました。ここにキャッシュとして機能しない理由は、次のように説明されています。
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 ステップを追加したという記述があり、早速このステップのリポジトリを確認すると2019年12月14日に廃止されており、利用できなくなっていました。
このリポジトリの Issue bitrise-steplib/steps-recursive-touch#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 が挙がっています。これが実現されれば、パッケージのキャッシュのみでビルド時間を大幅に削減できることになります!
(2021/2/19追記) FirebaseAnalytics のみビルド済の XCFramework で提供されるようになりました。SourcePackagesディレクトリ内の artifacts に配置されます。
結論
もうしばらくの間、Firebase iOS SDK などのビルド時間の長い大きなパッケージを含む場合は、ビルド時間を優先して Carthage を使い続けようと思います。
もし、他の良いソリューションがあればご教授ください🙏
参考文献
- CocoaPods でビルド済みバイナリを使用する
- CI で SwiftPM のキャッシュを利用する
- DerivedData をキャッシュする
- XCFramework について