kateinoigakukunのブログ

思考垂れ流しObserver

XcodeGenとCocoaPodsをいい感じに使う

背景

XcodeGenはXcodeのプロジェクトファイルをYAMLから生成するためのツールです。生のプロジェクトファイルは記述が冗長であったり、とても人間が手で管理できるものではないため、YAMLで管理しようというのがモチベーションです。 github.com

iOS/macOSアプリの開発においてライブラリ管理のデファクトスタンダードとなっているCocoaPodsというソフトウェアがあります。これはライブラリをビルドするためのプロジェクトファイル Pods.xcodeproj を生成し、メインのプロジェクトファイルと統合することでアプリにライブラリをインストールします。

さて、この2つのツールを組み合わせて使おうとすると、

  1. XcodeGenでプロジェクトファイルを生成
  2. pod install でプロジェクトファイルと Pods.xcpdeprojを統合

という2ステップの流れになります。しかし、pod install はキャッシュ機構があるとはいえ、ブランチをチェックアウトする度に実行するのはヘビーです。

そこでCocoaPodsとプロジェクトファイルの統合を事前に手動で行う方法を紹介します。この方法では Podfile.lockPods/Manifest.lockに差分がない場合においてはpod installを実装せずに済むため、プロジェクトファイルの生成時間を大幅に削減できます。

CocoaPodsの内部実装に依存する形になるのでCocoaPodsの変更に追従していく必要がありますが、やることは単純なので大まかな統合の処理は変わらないはずです。

詳細

実はCocoaPodsがプロジェクトファイルに行う処理はそこまで多くありません。

実際にpod install を行ったときに発生するxcodeprojへのdiffがこちらです。

gist.github.com

  1. ビルドをトリガーするためのダミーターゲット(Pods_XXX.framework)Dependenciesに追加
  2. CocoaPodsが生成したxcconfigをプロジェクトのベースconfigに設定(他のxcconfigが設定されていない場合のみ)
  3. EmbedするDynamic FrameworkをアプリにコピーするBuild Scriptを注入( [CP] Embed Pods Frameworks
  4. ライブラリが使うリソースをアプリにコピーするBuild Scriptを注入 ( [CP] Copy Pods Resources
  5. 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"'

サンプルプロジェクト

github.com

targetTemplatesというXcodeGenの素晴らしい機能を使って汎用的なproject.ymlにしているので、これをコピペするだけで大抵の環境では動くはずです。

Staticライブラリにしてる場合やabstract targetを使っている場合、多分動かないので適宜変更してください。