RettyでのPodBuilder導入事例

はじめに

 Rettyのアプリ開発チームでiOSアプリ/Androidアプリ/サーバーサイドエンジニアをしている山田です。 最近Rettyのアプリ開発チームでは「より高速な施策開発を行なっていきたい」「CIにかかるコストを削減したい」といったモチベーションから、特に時間がかかるiOSアプリのCIがより早く完了する状態にしたい、という声が上がるようになってきました。
これを実現するために日々いろいろな手法を模索しているのですが、今回はその文脈でPodBuilderと言うツールを試してみたので、その事例を共有したいと思います。

PodBuilderとは

 iOSアプリ開発ではお馴染みの依存関係マネージャーであるCocoaPodsですが、そのまま利用するとCIが実行される度にCocoaPods経由で導入した依存をビルドする必要があり、場合によってはこれが結構な時間になってしまいます。
 PodBuilderはこのような課題を解決するツールです。CocoaPodsで導入した依存を事前にFrameworkへビルドする事ができ、プロジェクトからは事前に生成されたFrameworkへ依存することで、アプリケーションをビルドするたびに発生する依存のビルド時間を省き、ビルド時間短縮を実現することができます。

github.com

PodBuilderの導入

導入手順

以下のような手順でRettyアプリへの導入を行いました。

  1. Gemfileにgem 'pod-builder'を追加
  2. bundle installを実行
  3. bundle exec pod_builder initを実行
  4. bundle exec pod_builder build_allを実行
  5. 成功すれば導入完了。失敗した場合は設定ファイルを調整し、4に戻る

 概ねReadMeに記載の手順に従って導入しています。 Rettyアプリの場合設定ファイルにいくつか調整が必要で、以下のような設定を追加で行っています。

spec_overrides の設定

ReadMeにも以下のように記載の通り、依存ライブラリのpodspecにswift_versionの記述が存在しない場合、エラーが発生します。

https://github.com/Subito-it/PodBuilder#i-get-an-podwitherror-does-not-specify-a-swift-version-and-none-of-the-targets-dummytarget-when-building

このような場合は適切なswift_versionを調べ、spec_overridesで適切な値を書き込むように設定する必要があります。

build_settings_overrides の設定

 追加でビルド時の設定が必要になる場合は、こちらで設定を行う必要があります。 例としてRettyアプリでは以下のようなケースに遭遇し、これを利用した設定を行っています。

  • 一部ライブラリが特定のバージョンのSwiftを利用でないとビルドができず、SWIFT_VERSIONを変更する必要があった
  • テストで利用するライブラリがENABLE_TESTABILITYをtrueに設定する必要があった

RettyでのPodBuilder.jsonの設定例

 一部を抜粋した形でRettyでの設定例を掲載します。 なお、この例は導入した場合にどのくらいのビルド時間削減効果を得られるのかを計測する目的でお試しをしている段階でのもので、実際に運用しているものではありません。 そのため、この設定例は本運用には適さない可能性があります。本運用を前提としてこれを参考にされる場合は、より設定を詰める必要がありそうです。

{
  "project_name": "Retty",
  "spec_overrides": {
    "Google-Mobile-Ads-SDK": {
      "module_name": "GoogleMobileAds"
    },
    "${specにバージョンの記載がないライブラリ}": {
      "swift_version": "5.0.0"
    },
  },
  "skip_licenses": [

  ],
  "skip_pods": [
    "Firebase",
    "GoogleMaps"
  ],
  "force_prebuild_pods": [
    "Firebase",
    "GoogleTagManager"
  ],
  "build_settings": {
    "ENABLE_BITCODE": "NO",
    "GCC_OPTIMIZATION_LEVEL": "s",
    "SWIFT_OPTIMIZATION_LEVEL": "-Osize",
    "SWIFT_COMPILATION_MODE": "wholemodule",
    "CODE_SIGN_IDENTITY": "",
    "CODE_SIGNING_REQUIRED": "NO",
    "CODE_SIGN_ENTITLEMENTS": "",
    "CODE_SIGNING_ALLOWED": "NO",
    "EXCLUDED_ARCHS[sdk=iphonesimulator*]": "arm64"
  },
  "build_settings_overrides": {
    "${ビルドするSwiftのバージョンを4.0に固定する必要のあるライブラリ}": {
      "SWIFT_VERSION": "4.0"
    },
    "${ENABLE_TESTABILITYの設定が必要なライブラリ}": {
      "ENABLE_TESTABILITY": true
    }
  },
  "build_system": "Latest",
  "license_filename": "Pods-acknowledgements",
  "subspecs_to_split": [

  ],
  "lfs_update_gitattributes": false,
  "lfs_include_pods_folder": false,
  "use_bundler": true
}

Rettyアプリでの効果

実際にRettyアプリに導入した際に、どのくらいの効果があったのかを紹介します。

Rettyアプリの構成紹介

まずは導入対象のRettyアプリの構成を紹介します。

Podでの依存の数

 RettyアプリはCocoaPods経由で38個の依存を導入しています。代表的なものとしては以下のような依存が存在します

  • Firebase
    • AnalyticsやCrashlytics、RemoteConfigなどを利用しています
    • Firestoreは利用していません
  • RealmSwift
  • Alamofire
  • Kingfisher
  • FBSDKLoginKit
  • LineSDKSwift

Rettyアプリのコード規模

882個のファイルからなる10万行ほどのSwiftのコードと、143個のxib, 72個のstoryboardが存在しています。

CIでのビルド時間

 以下の通り、Rettyで利用しているCIサービスであるBitriseのInsights機能によると、直近三ヶ月でのビルド時間はTypical run time(ステップの実行に要した時間の中央値*1 )で7m57.2sとなっています。このステップはfastlane scanを実行するもので、厳密にはビルドだけではなくテストのための時間も含まれてしまうものではありますが、今回はこの値でどのくらい改善されるのかを比較していきます。

f:id:rettydev:20220106155714p:plain
直近三ヶ月のBitriseでのビルド時間

検証結果

PodBuilder導入後にCIを5回実行し、計測したビルド時間が以下の通りです。

回数 ビルド時間
1回目 4m36s
2回目 4m30s
3回目 4m42s
4回目 4m42s
5回目 4m42s

平均: 4m38.4s
中央: 4m42s
最頻: 4m42s

という結果になりました。前述の通り、RettyアプリのCIでのビルド時間の中央値は7m57.2sなので、おおよそ3mと少し、割合にして40%のビルド時間を削減することに成功しています。繰り返しになりますがテストの時間も若干ながら含まれてしまっている値ではあるため、それを省けば50%近い削減効果がありそうです。ReadMeには

for a large project we designed this tool for we saw a 50% faster compile times, but YMMV

という記述があるのですが、まさにそれを実現できている感じです。非常に早くビルドが完了するようになり、かなり良い結果を得られました。

PodBuilderの問題点

 ここまででPodBuilderを導入することでビルド時間を大きく削減できることがわかりました。一方で、以下のような問題も存在します。

PodBuilder導入により依存管理の複雑さが増す

 PodBuilderを導入していなければ、CocoaPodsによる依存を追加する場合には

  1. PodBuilder/Podfileに追記
  2. bundle exec pod installを実行
  3. Podfile, Podfile.lockをcommit & push

するだけの単純な手順で済みます。一方でPodBuilderを導入すると

  1. Podfileに追記
  2. bundle exec pod_builder build_allを実行
  3. エラーが発生したら設定ファイルを調整し、再度2をやり直し
  4. Frameworkが出来たらこれを所定のストレージにアップロード
  5. PodBuilder/Podfile, PodBuilder/PodBuilder.json, Podfile, Podfile.lock, PodBuilder/Podfile.restoreをcommit & push

という手順となってしまい、複雑さの増加が否めません。ここはビルド時間削減とのトレードオフと考え、どちらを取るべきかチームでよく話し合うべきポイントかと思われます。

ビルド以外の部分での消費時間が増加すること

 単にビルド時間だけを見れば時間の削減には消費していますが、当然ビルド済みのFrameworkを取得する時間が追加で発生することは忘れてはいけません。Rettyアプリの場合、

  • とりあえず試してみるのが目的だった
  • 100MBを超えるバイナリが無かった
    • Rettyが利用しているGitHubでは100MBを超えるファイルをgitに含んでアップロードすることはできません

の二つの理由でビルド済みFrameworkはgitに直接含んで試しましたが、この場合では普段は4s程度で完了するcloneが10倍の40sもかかるようになってしまいました。Git LFSを使うなどの改善点もありますが、ひとまずそれなりのサイズのバイナリをダウンロードするにはそれなりの時間がかかるという部分は必ず考慮しなければならないです。

おわりに

 今回の記事ではiOSアプリのCIがより早く完了する状態にしたいというモチベーションから、PodBuilderの導入をお試ししてみた結果を紹介しました。結果は予想以上でしたが、導入に付随する他の問題点があったり、Carthageへ移行する、XCFrameworkを使うようにするなどの他の意見もあり、今後どうするのかというのは活発に議論がなされている最中です。

 Rettyのアプリ開発チームでは本記事のように日頃の開発体験を良くするためにどのような工夫ができるのか、というのを常にチームで話し合い、自ら改善を実施しながら開発を進めています。本記事でRettyのアプリ開発チームについて興味が湧きましたら、ぜひMeetyで気軽にお話ししましょう!

meety.net