[iOS][Carthage] ビルド時間短縮のため外部ライブラリの一部をCocoaPodsからCarthageに移行した


プロジェクトが育つにつれビルド時間が長くなって開発効率が落ちてきていたので、改善の一環としてネックとなっていたCocoaPods管理下のライブラリ群の一部をCarthageに移管した。
今更感はさておき復習も兼ねて記事化と共有。

1. Carthage インストール

Carthageが入っていない場合、インストール

$ brew update
$ brew install carthage

Carthageのバージョンが古い場合、アップデート

バージョン確認

$ carthage version
0.29.0

アップデート

$ brew upgrade carthage

.pkgでもインストール可能な模様。
https://github.com/Carthage/Carthage/releases

2. Cartfileの作成

Carthageのパッケージファイルを作成。

$ vi Cartfile
パッケージマネージャ CocoaPods Carthage Swift Package Manager
パッケージファイル Podfile Cartfile Package.swift
ロックファイル Podfile.lock Cartfile.resolved Package.pins

3. PodfileからCartfileに移行

3-1. Cartfileの編集

指定方法は極めてシンプル。
とりあえずAlamofireから入れてみる。

github "Alamofire/Alamofire"

バージョン指定もpodと書き方は同じ

# 4.5.0固定
github "Alamofire/Alamofire" == 4.5.0
# 4.5以降
github "Alamofire/Alamofire" >= 4.5
# 4.5と互換性があるバージョン
github "Alamofire/Alamofire" ~> 4.5

参考: ライブラリ管理ツールCarthageのCartfileの書き方
但し、互換指定に関してCocoaPodsとは結果が異なるため注意が必要。

3-2. ビルド

Cartfileを保存したら carthage update を実行してビルド。

$ carthage update --platform iOS

--platform iOS はプラットフォームの指定。macOSやwatchOSが必要な人は指定なしで。
--cache-builds というオプションもあってビルド済のライブラリはスキップしてくれる。ビルドキャッシュのバージョンとかもみてくれている模様。 xcode-select --switchした時も大丈夫。
--no-use-binaries はGitHubに上がっているバイナリデータを使わないオプション。0.20.0以降、このオプションが無くても自動的に判定してSwiftバージョンが異なる場合などソースコードからビルドしてくれるようになったらしいのでこれはもう不要。

carthage updatecarthage bootstrap は何が違うのかというと、
update はCartfileの記述からCartfile.resolvedにバージョンを書き込むのでアップデートされる可能性がある。
bootstrap はCartfile.resolvedで指定されているバージョンを使用する。

単にcheckoutしてビルドしたい時とか、CIでの実行は以下で実行するようにした。

$ carthage bootstrap --platform iOS --cache-builds

ビルドが終わると プロジェクトルート/Carthage/Build/iOS 以下に.frameworkができている。

3-3. Linked Frameworks and librariesに追加

プロジェクト > ターゲット > Generalタブ > Linked Frameworks and libraries に3-2でビルドした.frameworksを追加する。

+ > Add Other... を押して プロジェクトルート/Carthage/Build/iOS/Alamofire.framework を選択し追加。
直接FinderからDropしてもOK。

メインターゲットに自前のEmbed Frameworkを含んでいてそれも同じframeworkに依存している場合、そのframeworkのターゲット > Generalタブ > Linked Frameworks and libraries にも同様に追加しておく。

あとUnit TestやUI Testのターゲットにも同様に追加しておきましょう。

  • アプリのターゲット > Generalタブ > Linked Frameworks and libraries
  • Unit Testのターゲット > Generalタブ > Linked Frameworks and libraries
  • UI Testのターゲット > Generalタブ > Linked Frameworks and libraries
  • 自前frameworkのターゲット > Generalタブ > Linked Frameworks and libraries

3-4. Build Phaseの追加

プロジェクト > ターゲット > Build PhaseにRun Scriptを追加する。
Shellの内容

/usr/local/bin/carthage copy-frameworks

Input Filesの追加

$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework

UI Testのターゲットにも同様の追加が必要。

3-5. podからの除外

Podfileの pod 'Alamofire' の記述を削除して pod install しておく

4. lint対象からの除外

lintツールとか入れている場合、そのままビルドするとframework内の記述が怒られてしまいビルドが通らないので、対象から除外しておく必要がある。
SwiftLintの場合

.swiftlint.yml
...
excluded:
  - Pods
  - Carthage # 追加
...

xcode-select

複数のXcodeバージョンを使い分けている人は、意図せずxcode-selectの向き先が想定していないXcodeバージョンを向いていることがあり、プロジェクトの設定とframeworkのSwiftのバージョンが合わない場合にビルドエラーになるので要チェック。

確認

$ xcode-select -print-path

変更

$ sudo xcode-select --switch /Applications/Xcode8.3.3.app

5. ライセンス表示

CocoaPodsでは自動的にライセンス表示のためのplistが作られるのでPodfileでSettings.bundle内にコピーすれば良いだけだったが、Carthageはシンプルが故に自前で行う必要がある。
併用しているのでCocoaPodsが作ったplistをベースにCarthage管理下のライブラリのライセンス情報を追加して新たなplistを生成してSettings.bundle内にコピーするシェルを書いてみた。(もっと効率良い書き方や方法があったらお教えください)

acknowledgements.sh
#!/bin/sh 

if [ $# -ne 1 ]; then
  echo "Error: Parameter is required."
  exit 1
fi

# Move to project root directory
cd $1

# Copy plist file to Settings.bundle
inPlistPath="Pods/Target Support Files/Pods-<APP_NAME>/Pods-<APP_NAME>-acknowledgements.plist"
outPlistPath="<APP_NAME>/Settings.bundle/Acknowledgements.plist"
cp "$inPlistPath" $outPlistPath

# Read contents of Carthage/Checkouts
directories="Carthage/Checkouts/*"

#  Add license to plist
for directory in $directories; do

  licenseTextPath="${directory}/LICENSE"
  licenseText=`cat $licenseTextPath`
  libraryName="`basename $directory`"
  item="<dict><key>FooterText</key><string>${licenseText}</string><key>Title</key><string>${libraryName}</string><key>Type</key><string>PSGroupSpecifier</string></dict>"
  keyPath="PreferenceSpecifiers.0"

  # Insert to plist file
  plutil -insert $keyPath -xml "$item" $outPlistPath

done

これをメインターゲット > Build Phase > New Run Script Phase して実行しておけば、ビルドごとに勝手に上書きされるようになる。

${PROJECT_DIR}/acknowledgements.sh ${PROJECT_DIR} 

6. プロジェクトをビルド

念のためCleanしてからビルド。
他のライブラリも同様に移行したい場合、ステップ3を行う。

7. その他

自分の場合、移行に伴い下記のタスクも必要になった。
- CI環境でCarthageに対応しておく。
- プロジェクトに README.md とか含んでいる場合、Carthageの記述を追加しておく。

感想

下記のライブラリをCarthageにしたがビルドが非常に早くなった!

github "Alamofire/Alamofire"
github "realm/realm-cocoa"
github "johnsundell/unbox"
github "mxcl/PromiseKit"
github "SnapKit/SnapKit"

自前で対応しないとならない部分もあるけど、いろいろ勝手に作られないし、何よりもわかりやすい。将来的にはCocoaPodsは廃止したいなぁ。
Bitrise でもCarthageステップを追加して対応し、2回目以降はworkflow実行時間が30%ほど削減できた


Carthageで必ずソースコードからインストールする場合
コマンドラインでplistを操作(データ追加・編集・削除)