iOS開発で、Romeを使ってCarthageのビルド成果物をキャッシュする

この記事は、[株式会社アトラエ アドベントカレンダー](https://qiita.com/advent-calendar/2020/atrae)16日目の記事です。
前日の記事は、[@tsucchi_13](https://twitter.com/tsucchi_13)の「[安全かつクイックなリリースのために、Git flowを拡張してみた](https://qiita.com/tsucchi13/items/0aa1b30f83c2db628fd9)」でした。

本日はエンジニアの [@ysk_aono](https://twitter.com/ysk_aono)が担当します。直近の業務では、iOSアプリの開発やデータ分析基盤の整備などを行っています。

## Carthageのビルドは...なかなか待たされる

iOSアプリの開発を行う際、3rdパーティーのライブラリ導入時etc.にパッケージマネージャを使うのが普通だと思います。
CocoaPodsやCarthage、Swift Package Managerなどが代表例として挙げられるでしょうか。

今回のテーマの[Carthage](https://github.com/Carthage/Carthage)には、
「導入時のビルドでライブラリのフレームワークを作成するため、開発時のアプリビルドのコンパイル時間が短縮される」
といったメリットが主にあるかと思います。

非常に便利な反面、ここで課題になりえるのが**事前ビルドのコスト (時間)** です。`carthage bootstrap`を気軽に実行すると、それなりの待ち時間を要することも多いかと思います。 

![pose_iraira_matsu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1795/78347422-a983-ae88-e58f-e546e2bf853c.png)

特に顕著なのが、[Bitrise](https://www.bitrise.io/)のようなCI as a Serviceでビルドを行う時です。
もちろんどのサービスにもキャッシュ機構は備わっており、少し設定を追記すればライブラリ成果物をキャッシュすることは可能ですが、

- 初回やライブラリ更新 (CIサービス上にキャッシュが無い状態) , Xcodeアップデート時のビルドは、時間が非常にかかる
- そのサービス特有のオペレーションを実行する必要があり、デプロイスクリプト (fastlane) のポータビリティが失われがち
    - 例えばBitriseでは、独自の[Carthageステップを使う必要がある](https://devcenter.bitrise.io/jp/builds/caching/caching-carthage/)

といった点をもう少し良くしたいと考えました。

特に1つめに挙げた初回/更新時のビルド時間は辛いところで、
例えばRealmのようなビルドコストの大きなライブラリを利用している場合、CIサービスのビルド時間上限に達してしまい「ぐぬぬ...終わらねぇ...」となることも多いです。

（もちろん札束で殴っていく手段もあるのですが、コスト観点も踏まえてそちらの方法はまだ採用していません)

### あとから追記

Carthage経由のフレームワークをバージョン管理に含めるという手段も存在しますが、
Gitリポジトリの容量 (と操作) が明らかに重くなるなどのデメリットも見込まれるため、今回は検討外としています

## Rome

現在、iOSアプリの開発をかなり少人数 (私以外に1~2人) で行っています。
ライブラリのアップデートについても私が定期的なタイミングで実行することが多く、利用するライブラリバージョンが開発者の間でズレることはほぼありません。

つまり、例えば自身のマシンからCarthageビルド成果物のキャッシュを作ってそれをCIから利用できると、もう少し幸せな感じになりそうです。それを解決してくれそうなのが [Rome](https://github.com/tmspzz/Rome) でした。

Romeが提供してくれる機能の概要ですが、公式の[README](https://github.com/tmspzz/Rome/blob/master/README.md)に書いている通りです。
例えばAmazon S3を、Carthageでビルドした成果物のキャッシュストアとして利用することができます。

> Rome is a tool that allows developers on Apple platforms to use:

> - Amazon's S3
> - Minio
> - Ceph
> - other S3 compatible object stores
> - or/and a local folder
> - your own custom engine

> as a shared cache for frameworks built with Carthage.

## どうやって使うのか

どうやって使うのか、思い出しながら書きます。

### 1/ インストール

導入はCocoaPodsかHomebrew経由で行えますが、CocoaPodsを採用しました。
iOSプロジェクトのルートディレクトリで、`Pods/Rome/rome`コマンドを使えるようになります。

```bash
> Pods/Rome/rome --help
Cache tool for Carthage

Usage: rome COMMAND [--romefile PATH] [-v]

Available options:
  -h,--help                Show this help text
  --version                Prints the version information
  --romefile PATH          The path to the Romefile to use. Defaults to the
                           "Romefile" in the current directory.
  -v                       Show verbose output

Available commands:
  upload                   Uploads frameworks and dSYMs contained in the local
                           Carthage/Build/<platform> to S3, according to the
                           local Cartfile.resolved
  download                 Downloads and unpacks in Carthage/Build/<platform>
                           frameworks and dSYMs found in S3, according to the
                           local Cartfile.resolved
  list                     Lists frameworks in the cache and reports cache
                           misses/hits, according to the local
                           Cartfile.resolved. Ignores dSYMs.
  utils                    A series of utilities to make life easier. `rome
                           utils --help` to know more
```

コマンド名の通りですが、`rome upload`でキャッシュのアップロード/更新、`rome download`でキャッシュのダウンロードができます。

### 2/ Romefileの作成

Romeでは、動作の設定を`Romefile`にyaml形式で記述していきます。
ファイルの置き場所は`Cartfile`と同じ階層にする必要があるので、だいたいプロジェクトのルートディレクトリになるかと思います。

設定といっても大した記述量ではなく、**「どこにキャッシュするか」**, **「リポジトリとキャッシュしたいフレームワーク名の対応(※リポジトリ名とフレームワーク名が異なる場合)」**を書けばとりあえず動作するようになるはずです。
キャッシュ場所を1つ以上セットすることが必須条件となります。

※その他Romefileに記述できるブロックについては、[公式README](https://github.com/tmspzz/Rome/blob/master/README.md#romefile)に載っています。

#### `cache`の指定

今回のケースでは、Amazon S3をキャッシュのアップロード場所として利用するので、以下のように書きます。

```yaml:Romefile
cache:
    s3Bucket: ios-library-build-cache
```

`s3Bucket`に指定しているのは、S3に存在するバケット名です。(事前に作成が必要です)
ここにビルド成果物のキャッシュが入っていくことになります。

#### `repositoryMap`の指定

ライブラリの中に複数のフレームワークが含まれる場合・リポジトリがGitHub上にない場合など、
リポジトリ名とフレームワーク名が一致しないケースがしばしば存在するかと思います。

そういった場合、Romeはフレームワークの名前解決を行えません。
そこで使うのが`repositoryMap`です。ライブラリの中でキャッシュしたいフレームワークを名前の中で指定します。

例えばRxSwiftの中のフレームワークの一部をキャッシュしたい場合は、以下のように記述します。

```yaml:Romefile
repositoryMap:
- RxSwift:
    - name: RxSwift
    - name: RxCocoa
    - name: RxRelay
```

### 3/ S3の認証が通るようにする

Amazon S3とデータのやりとりをするわけなので、認証情報を持った状態でRomeを実行する必要があります。
作成したS3バケットにアクセスできるIAMを用意し、おなじみのアクセスキーとシークレットキーを取得します。

### 4/ キャッシュのアップロード

これで、Macで`carthage bootstrap`して作成したCarthageのビルド成果物を、S3にアップロードできるようになります。

環境変数でAWSの認証情報とバケットの置かれたリージョンを指定し、以下を実行するとS3へのアップロードが開始されます。

```bash:
AWS_REGION=ap-northeast-1 \
AWS_ACCESS_KEY_ID=XXX \
AWS_SECRET_ACCESS_KEY=XXX \
Pods/Rome/rome upload \
--platform iOS \
--cache-prefix `xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"`
```

今回はiOSアプリを想定しているので`--platform`にはiOSを、
`--cache-prefix`には、キャッシュを保存する際のプレフィックス（バケット内でのディレクトリ名）を、指定します。

ちなみに`--cache-prefix`にセットしているコマンドですが、実行すると以下のような結果が得られます。
Swiftのバージョン番号をキャッシュのキー名にし、同じXcode環境のCIで後から取り出す時に利用できるようにしています。

```bash
> xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"

swiftlang-1200.0.41-clang-1200.0.32.8
```

Rxswiftであれば、「作成したバケット > swiftlang-1200... > RxSwift」下にこんなファイルとディレクトリができ、

![Screen Shot 2020-12-16 at 6.57.18 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1795/9b57888e-8692-9b84-8b71-3c582bf37f60.png)

さらにその下にはフレームワークをzipで固めたやつが置いてあります。

![Screen Shot 2020-12-16 at 6.57.44 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1795/fd62b9fe-d66c-13bb-0256-ca243abb905d.png)

手元でキャッシュを更新する場合は、
`carthage update`とセットで`rome upload`を実行するシェルスクリプトを作っておくといいかもしれませんね。

### 5/ キャッシュのダウンロード

先ほどと逆の操作です。
CI上や他の開発マシンからキャッシュをDLしたいときは以下を実行すると、キャッシュされたフレームワークがプロジェクトの`Carthage/Build`下に入っていきます。

```bash
AWS_REGION=ap-northeast-1 \
AWS_ACCESS_KEY_ID=XXX \
AWS_SECRET_ACCESS_KEY=XXX \
Pods/Rome/rome download \
--platform iOS \
--cache-prefix `xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"`
```

これをCI上で実行できれば、
Carthageのビルド時間がS3からのDL時間に置き換わり、良い感じになりそうです！

## CIサービス上でのキャッシュダウンロード

CIサービス(Bitrise)でのアプリビルドには現在fastlaneのスクリプトを利用しており、こちらでRomeのキャッシュダウンロードを行います。

以下がその一部になります。
先述したRomeの実行を、アプリのビルド前にやっているだけです。
これで、Xcodeのバージョンが合ってさえいればCarthageのビルドを行う必要がなくなりました！！！！１

```ruby:Fastfile
platform :ios do
  desc 'Upload a new AdHoc build'
    lane :adhoc do
      Dir.chdir(ENV['PWD']) do
        set_up_carthage
      end

      ...

      build_ios_app

      ...
    end
  end

  desc 'Set up carthage'
  private_lane :set_up_carthage do
    sh("Pods/Rome/rome download --platform iOS --cache-prefix #{rome_cache_prefix} --concurrently")
  end

  def rome_cache_prefix
    sh("xcrun swift --version | head -1 | sed 's/.*\\((.*)\\).*/\\1/' | tr -d \"()\" | tr \" \" \"-\"").chomp
  end
end
```

※AWSの認証情報などは、環境変数としてCIサービスに登録が必要です。
※`ENV['PWD']`には、Bitrise上でアプリのソースコードが置かれている`/Users/vagrant/git`を指定しています。

## 蛇足 : ライブラリのアップデートも自動でやる ※あまりうまくいっていない

ライブラリのアップデートもCIのワークフローとしてやりたいなと思い、Romeを絡めてfastlaneのコードを書いてみました。

```ruby:Fastfile
platform :ios do
  desc 'Update libraries and create PR'
  lane :update_libraries do
    Dir.chdir(ENV['PWD'])

    set_up_carthage
    sh('carthage', 'update', '--platform', 'iOS', '--cache-builds', '--no-use-binaries')
    sh("Pods/Rome/rome upload --platform iOS --cache-prefix ci-update-#{rome_cache_prefix} --concurrently")

    # GitHubでPullRequestを作成
    # ...
  end

  desc 'Upload updated carthage build to S3'
  lane :upload_updated_carthage_build do
    Dir.chdir(ENV['PWD'])
    sh("Pods/Rome/rome download --platform iOS --cache-prefix ci-update-#{rome_cache_prefix} --concurrently")
    sh("Pods/Rome/rome list --missing --platform iOS --cache-prefix #{rome_cache_prefix} | awk '{print $1}' | xargs -I {} rome upload \"{}\" --platform iOS --cache-prefix #{rome_cache_prefix} --concurrently")
  end
end
```

上記の2つのワークフローはざっくり下記のようなことを行っており、
組み合わせて使えば定期的なライブラリ更新を実現できそうです。

- `carthage update` → 普段とは異なるプレフィックス (ci-update-xxx) をつけてキャッシュ保存 → GitHubでライブラリ更新のPR作成
- ci-update-xxxプレフィックス付きのキャッシュを取得 → 通常プレフィックスのキャッシュ上でヒットしなかったものを保存し直す

しかし、`carthage update`の実行時間が長くビルド上限に引っかかることが多いため、
現状だとうまく機能しているとはいえません:cry:

## おわりに

Romeのおかげで、Carthageのビルド成果物をお手軽にキャッシュできるようなりました。
Carthageがもたらしてくれる開発の快適さ、継続的なビルドや配信の速度、fastlaneスクリプトのポータビリティを担保することができました。

※~~最近CarthageをSwiftPMに移行することを検討して試行錯誤し、まだうまくいっていないためにCarthageをそのまま利用していることは黙っておきます。~~ Carthageもありがとう!!

以上になります。

明日は、フロントエンドエンジニアの [@kato5315](https://qiita.com/kato5315)が書きます！


**PR**
Greenやwevox, yentaといったサービスを提供する弊社アトラエですが、
現在、世界中を魅了するプロダクトを作りたいエンジニアを[大募集](https://www.green-japan.com/company/172)しています!!