iOSアプリのCI（CircleCI）を改善してみた話

この記事は、Classi Advent Calendar 2020 12/21担当の記事です。

Classi プロダクト開発部 iOSエンジニアの@yokoyanです。

モバイルアプリチームでは、CIの運用が、初期に構築したメンバーが退職していることもあり、ブラックボックスになりかけていたので、整備をおこなったところ、あまり事例がなさそうな方法で改善してみたので、iOSエンジニアにも役に立ちそうなネタが出来たかなと思ったので、それを紹介したいと思います。

# なぜ、改善することになったか
きっかけは、Carthageのセグメンタルエラーが発生する事象への対応です。
[Carthage crashes with Segmentation Fault: 11 · Issue #2760 · Carthage/Carthage](https://github.com/Carthage/Carthage/issues/2760)

このエラーは、Carthageのバージョンを0.35.0以上にあげれば、解消されます。

CircleCIは、Carthageがデフォルトでインストールされている
発生時点で、CircleCIのCarthageのバージョンは0.33.0でした。

方法は二つありました。

1. デフォルトでインストールされているCarthageをアップデートする
1. パッケージ管理ツールを使って任意のバージョンをインストール出来るようにする

# 方法１ デフォルトでインストールされているCarthageをアップデートする
```shell:.circleci/config.yml
commands:
  setup_homebrew:
    description: "Setup Homebrew with caches"
    steps:
      - restore_cache:
          keys:
            - v1-brew-{{ epoch }}
            - v1-brew-
      - run: brew update
      - run:
          name: Homebrew install or upgrade Carthage
          command: bash ./script/carthage-install.sh
      - run: brew cleanup
      - save_cache:
          key: v1-brew-{{ epoch }}
          paths:
            - /Users/distiller/Library/Caches/Homebrew
            - /usr/local/Homebrew
  setup_carthage:
    description: "Setup carthage with caches"
    steps:
      - run:
          name: Set Xcode Version
          command: echo `xcodebuild -version` > XCODE_VERSION
      - restore_cache:
          keys:
            - v1-ca-{{ checksum "XCODE_VERSION" }}-{{ checksum "Cartfile.resolved" }}
            - v1-ca-
      - run:
          name: Carthage bootstrap
          command: carthage bootstrap --platform iOS --cache-builds
      - save_cache:
          key: v1-ca-{{ checksum "XCODE_VERSION" }}-{{ checksum "Cartfile.resolved" }}
          paths:
            - Carthage
```
```shell:scripts/carthage-install.sh
#!/bin/sh -x
which carthage;
if [ $? -eq "0" ]; then
  brew outdated carthage || brew upgrade carthage
else
  brew install carthage
fi
```

基本的には、```brew upgrade``` でCarthageのバージョンを上げられるので、極論、↓でいいとは思います。

```shell
- run: command: |
    brew update
    brew upgrade
    brew cleanup
```

ただ、この方法では、全てのライブラリがアップデートされてしまい、関係ないライブラリがアップデートされ、別の問題が発生する可能性もあるので、一旦、Carthageだけアップデートするようにした方がいいです。

それと、carthage-install.sh というスクリプトを作って、
```brew outdated carthage || brew upgrade carthage```しているのは、バージョンが最新であれば、余計なコマンド実行をしないようにしています。

# 方法２ パッケージ管理ツールを使って任意のバージョンをインストール出来るようにする
次に、MintやSwift Package Manager などのパッケージ管理ツールを使えば、Carthageの任意のバージョンを指定してインストールする場合です。方法１に少し、手順を追加したものです。
自分が、担当しているアプリは、Mintを使っているので、Mintでの方法になります。

```shell:.circleci/config.yml
commands:
  setup_homebrew:
    description: "Setup Homebrew with caches"
    steps:
      - restore_cache:
          keys:
            - v1-brew-{{ epoch }}
            - v1-brew-
      - run: brew update
      - run:
          name: Homebrew install or upgrade Mint
          command: bash ./script/mint-install.sh
      - run: brew cleanup
      - save_cache:
          key: v1-brew-{{ epoch }}
          paths:
            - /Users/distiller/Library/Caches/Homebrew
            - /usr/local/Homebrew
  setup_mint:
    description: "Setup Mint with caches"
    steps:
      - restore_cache:
          keys:
            - v1-mint-{{ checksum "Mintfile" }}
            - v1-mint-
      - run:
          name: Mint install
          command: |
            mkdir -p $MINT_PATH/{lib,bin}
            mint bootstrap --link
          environment:
            MINT_PATH: /Users/distiller/output/mint
            MINT_LINK_PATH: /Users/distiller/output/mint/bin
      - save_cache:
          key: v1-mint-{{ checksum "Mintfile" }}
          paths:
            - mint
  setup_carthage:
    description: "Setup carthage with caches"
    steps:
      - run:
          name: Set Xcode Version
          command: echo `xcodebuild -version` > XCODE_VERSION
      - restore_cache:
          keys:
            - v1-ca-{{ checksum "XCODE_VERSION" }}-{{ checksum "Cartfile.resolved" }}
            - v1-ca-
      - run:
          name: Carthage bootstrap
          command: $MINT_PATH/bin/carthage bootstrap --platform iOS --cache-builds
      - save_cache:
          key: v1-ca-{{ checksum "XCODE_VERSION" }}-{{ checksum "Cartfile.resolved" }}
          paths:
            - Carthage
```
```shell:scripts/mint-install.sh
#!/bin/sh -x
which mint;
if [ $? -eq "0" ]; then
  brew outdated mint || brew upgrade mint
else
  brew install mint
fi
```
```shell:Mintfile
Carthage/Carthage@0.36.0
```

ポイントは、Carthageのインストールを、Mintで行っているところです。
こうすることにより、Carthageのバージョンを指定できます。
※この場合、CircleCIのデフォルトでインストールされているCarthageを誤って使ってしまわないように注意

Mintの使い方は、以下のサイトをご参照ください。
[Swift製コマンドラインツールのパッケージ管理ツール「Mint」のセットアップ&操作方法 - Qiita](https://qiita.com/uhooi/items/6a41a623b13f6ef4ddf0)

# Homebrewを利用するのをやめて、Swift Package Managerでインストールする
Carthageをアップデートすることは出来るようになりましたが、HomebrewはキャッシュをRestoreするだけでも30secかかるので、Homebrew以外の方法がないかを検討してみました。

Mintのインストール方法としては、Homebrewでインストールする方法の他に、MakeやSwift Package Managerでインストールする方法があります。
https://github.com/yonaskolb/Mint#installing

このうち、Makeは、その時点で、問題があったので、今後、Swift製のツールをまとめることも考えて、Swift Package Managerを、使った方法も紹介します。

```shell:.circleci/config.yml
commands:
  setup_mint_tools:
    description: "Setup Mint tools with caches"
    steps:
      - restore_cache:
          name: SPM restore cache
          keys:
            - v1-spm-mint-{{ arch }}-{{ checksum "MintTools/Package.resolved" }}
      - run:
          name: Build SPM for Mint
          command: ./MintTools/.build/release/mint --version || swift run -c release --package-path MintTools --build-path MintTools/.build mint --version
      - save_cache:
          name: SPM save cache
          key: v1-spm-mint-{{ arch }}-{{ checksum "MintTools/Package.resolved" }}
          paths:
            - MintTools
```

# その他、やったこと
1. aliasesをやめて、commandにする（それに伴って、config.ymlのバージョンも2から2.1にアップ）
1. bundle install に、並列処理の設定（BUNDLE_JOBS: 4）を追加
1. 対応するキャッシュが見つからなかった場合に、保存されている中で最新のキャッシュを取得するようにした
1. キャッシュのキーのプレフィックスに「v1」を追加
1. Xcodeのバージョンをアップする時にエラーにならないように、Carthageのキャッシュのキーにxcodeのバージョンを追加
1. マシンが変わってもエラーにならないようにする


### aliasesからcommandsに変更（それに伴って、config.ymlのバージョンも2から2.1にアップ）
紹介した、commandsの中のsetup_carthageは、もともと、以下のように、aliasesで書いてました
これだと、扱いづらく、説明文も記載できないので、commandsに変更しています。もともと、ymlのバージョンを２だったのを、2.1に変更して、commandsに書き換えました。

```shell
aliases:
  - &step_restore_carthage_cache
    restore_cache:
        key: ca-{{ checksum "Cartfile.resolved" }}
  - &step_carthage_bootstrap
    run:
      name: Carthage
      command: $MINT_PATH/bin/carthage bootstrap --platform iOS --cache-builds
  - &step_save_carthage_cache
    save_cache:
      key: ca-{{ checksum "Cartfile.resolved" }}
      paths:
        - Carthage
```

### bundle install に、並列処理の設定（BUNDLE_JOBS: 4）を追加
iOSアプリの開発では、cocoapodsのバージョン管理をするために、bundlerを使うことがよくあるかと思いますが、
その、bundle installを、オプションで並列処理の設定をすると、高速化できるらしいので設定されてなかったのを設定したりしました。
[安全で高速なbundle install（ディレクトリを指定&並列化&省略オプション） - Qiita](https://qiita.com/jshimazu/items/4de4a53e3c98f0495933)
### 対応するキャッシュが見つからなかった場合に、保存されている中で最新のキャッシュを取得するようにした
参考
[依存関係のキャッシュ - CircleCI](https://circleci.com/docs/ja/2.0/caching/#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%AE%E5%BE%A9%E5%85%83)
[【ruby×circleci】はじめてのCI環境の構築④ / キャッシュを使ってビルドを効率化する - 日々の学びのアウトプットするブログ](https://ushinji.hatenablog.com/entry/2018/11/20/004542)
### キャッシュのキーのプレフィックスに「v1」を追加
キャッシュは上書き不可の為、依存関係管理ツールのバージョンや言語のバージョンが変更された場合、プロジェクトから依存関係が削除された場合などに、キャッシュを参照すると問題が出た場合など、数字を変更してキャッシュをクリア(参照しない)できるように
参考
[依存関係のキャッシュ - CircleCI](https://circleci.com/docs/ja/2.0/caching/#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%AE%E3%82%AF%E3%83%AA%E3%82%A2)
### Xcodeのバージョンをアップする時にエラーにならないように、Carthageのキャッシュのキーにxcodeのバージョンを追加
参考
[ios circleciでcarthageのキャッシュが効かなかった原因と対応 - とりあえずphpとか](https://kimagureneet.hatenablog.com/entry/2019/01/30/222636)

### マシンが変わってもエラーにならないようにする
最初、キャッシュの上書きが出来ないので、epoch を使って、毎回、キャッシュを作成し、リストア時には、前方一致で最新のキャッシュを取り出してました

```shell
- restore_cache:
    keys:
      - v1-brew-
- save_cache:
    key: v1-brew-{{ epoch }}
```
これだと、マシンが変わったときに、問題になりそうだということで、archを追加してみました。

```shell
- restore_cache:
    keys:
      - v1-brew-{{ arch }}-
- save_cache:
    key: v1-brew-{{ arch }}-{{ epoch }}
```
ただ、これも無駄が多いので、最終的に、Homebrewをやめ、Swift Package Managerを使って、Package.resolved を使ったchecksumで、必要な分のキャッシュが作成され、ピンポイントでリストアすることができるようになったので、何をリストアしているのか明確に把握できるようになりました。

# まとめ
この記事を見たかたが、参考になったらな嬉しいなと思いますが、詳細は書きれていないので、もし、質問があれば、出来るだけ回答しようと思っています。
今回、いろいろ改修したおかげで、可読性も保守性も、かなり改善しました。
おかげで、いろいろ勉強になりました。
これをやる直前まで、Bitriseに乗り換えも検討していましたが、慣れると、CircleCIの方が、設定ファイルを見れば、何をしているかがシンプルで分かりやすいので、いいなと感じました。