XcodeGenとCocoaPodsをいい感じに使う
背景
XcodeGenはXcodeのプロジェクトファイルをYAMLから生成するためのツールです。生のプロジェクトファイルは記述が冗長であったり、とても人間が手で管理できるものではないため、YAMLで管理しようというのがモチベーションです。 github.com
iOS/macOSアプリの開発においてライブラリ管理のデファクトスタンダードとなっているCocoaPodsというソフトウェアがあります。これはライブラリをビルドするためのプロジェクトファイル Pods.xcodeproj
を生成し、メインのプロジェクトファイルと統合することでアプリにライブラリをインストールします。
さて、この2つのツールを組み合わせて使おうとすると、
- XcodeGenでプロジェクトファイルを生成
pod install
でプロジェクトファイルとPods.xcpdeproj
を統合
という2ステップの流れになります。しかし、pod install
はキャッシュ機構があるとはいえ、ブランチをチェックアウトする度に実行するのはヘビーです。
そこでCocoaPodsとプロジェクトファイルの統合を事前に手動で行う方法を紹介します。この方法では Podfile.lock
と Pods/Manifest.lock
に差分がない場合においてはpod install
を実装せずに済むため、プロジェクトファイルの生成時間を大幅に削減できます。
CocoaPodsの内部実装に依存する形になるのでCocoaPodsの変更に追従していく必要がありますが、やることは単純なので大まかな統合の処理は変わらないはずです。
詳細
実はCocoaPodsがプロジェクトファイルに行う処理はそこまで多くありません。
実際にpod install
を行ったときに発生するxcodeprojへのdiffがこちらです。
- ビルドをトリガーするためのダミーターゲット(Pods_XXX.framework)Dependenciesに追加
- CocoaPodsが生成したxcconfigをプロジェクトのベースconfigに設定(他のxcconfigが設定されていない場合のみ)
- EmbedするDynamic FrameworkをアプリにコピーするBuild Scriptを注入(
[CP] Embed Pods Frameworks
) - ライブラリが使うリソースをアプリにコピーするBuild Scriptを注入 (
[CP] Copy Pods Resources
) pod install
が実行されていない場合にビルドをfailさせる Build Scriptを注入([CP] Check Pods Manifest.lock
)
だけです。ライブラリのビルドをトリガーする部分はXcodeのビルドシステムに乗っているため、特に考えることはないです。 それぞれ順番に何をするか説明します。
0. (下準備)
CocoaPodsがプロジェクトファイルに手を加えないようにPodfileで integrate_targets: false
を指定します。
install! 'cocoapods', integrate_targets: false target 'XcodeGenWithPods' do use_frameworks! pod "Alamofire" end
あと、生成されたPods.xcodeprojとメインのプロジェクトファイルを束ねるxcworkspaceを作っておきます。通常はCocoaPodsが生成しますが自動統合を切ったため自分で用意する必要があります。
1. ビルドをトリガーするためのダミーターゲット(Pods_XXX.framework)Dependenciesに追加
CocoaPodsのビルドシステムは自前で色々やっているので基本的にXcodeのビルドシステムに依存していません。ただ、そのままではXcodeのビルドボタンを押した時に何もビルドされないので、最初のビルドを発火させるためダミーターゲットを用意しています。
これを依存ターゲットとしてプロジェクトファイルに追加する必要があります。プロジェクトファイルを跨いだフレームワークの参照なので、project.ymlにimplicit
オプションを付ける必要があります。
dependencies: - framework: "Pods-${target_name}.framework" implicit: true embed: false
2. CocoaPodsが生成したxcconfigをプロジェクトのベースconfigに設定
これはCocoaPodsを使いつつxcconfigを使うときと同じテクニックです。CocoaPodsはアプリのビルド時に正しくヘッダを探したり、ライブラリをリンクするために様々なフラグをxcconfig経由で渡しています。そのため、XcodeGenからプロジェクトファイルを生成する際にはそれを指定する必要があります。
targets: "XcodeGenWithPods": configFiles: Debug: Pods/Target Support Files/Pods-${target_name}/Pods-${target_name}.debug.xcconfig Release: Pods/Target Support Files/Pods-${target_name}/Pods-${target_name}.release.xcconfig
3, 4, 5. Build Scriptを注入
Dynamicなフレームワークとしてライブラリをビルドした場合、実行時に.frameworkファイルを参照するので成果物をアプリにコピーします。 また、Staticなlibraryとしてビルドするとリソースをアプリに直接コピーする必要があるので、その処理を発火させます。
これら2つのシェルスクリプトはCocoaPodsが必要に応じて生成するので、存在しなければ実行する必要はありません。
preBuildScripts: - name: "[CP] Check Pods Manifest.lock" path: /bin/sh script: | diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null if [ $? != 0 ] ; then # print error to STDERR echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2 exit 1 fi # This output is used by Xcode 'outputs' to avoid re-running this script phase. echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}" inputFiles: - "${PODS_PODFILE_DIR_PATH}/Podfile.lock" - "${PODS_ROOT}/Manifest.lock" outputFiles: - "$(DERIVED_FILE_DIR)/Pods-${target_name}-checkManifestLockResult.txt" postCompileScripts: - name: "[CP] Embed Pods Frameworks" path: /bin/sh script: '"${PODS_ROOT}/Target Support Files/Pods-${target_name}/Pods-${target_name}-frameworks.sh"' - name: "[CP] Copy Pods Resources" path: /bin/sh script: '"${PODS_ROOT}/Target Support Files/Pods-${target_name}/Pods-${target_name}-resources.sh"'
サンプルプロジェクト
targetTemplates
というXcodeGenの素晴らしい機能を使って汎用的なproject.ymlにしているので、これをコピペするだけで大抵の環境では動くはずです。
Staticライブラリにしてる場合やabstract targetを使っている場合、多分動かないので適宜変更してください。