Build Numberを自動設定する方法いろいろ


iOS2 Advent Calendar 2017の20日目!

なにを書こうか迷ったのですが、より多くの人が関係する事がよいだろうなーといことで、この題材に。
既知のことが多いと思いますが、知らないことが少しでも含まれていれば嬉しいな!

目的

Build Numberを自動設定する方法について、以下の目的を前提に、どんな方法があるか調査してみました。

  1. 外部ライブラリは使わず、XCodeだけでできること
  2. 操作はXCodeでビルドするだけであること(CIは使わない)
  3. 複数のTARGETに対応できること(メインのiOSの他にWatch appや各Extensionがある)
  4. 更新したBuild NumberはGitの管理対象外にできると幸せ

1. Apple Generic Versioning Tool を使う

略してagvtoolと言うらしい。Version、Build Numberを自動的にインクリメントして設定してくれます。
Technical Q&A QA1827: Automating Version and Build Numbers Using agvtool

Build Settings で Versioning > Version System に "Apple Generic"を設定し、Current Project Versionに今のBuild Numberを設定します。

コマンドラインで、プロジェクトルートに移動し、

agvtool next-version -all

をすると、バージョンが1つ上がり、全てのplistのBuild Numberが更新されます。

$ agvtool next-version -all

Setting version of project Workout to: 
    101.

Also setting CFBundleVersion key (assuming it exists)

Updating CFBundleVersion in Info.plist(s)...

Updated CFBundleVersion in "Workout.xcodeproj/../Watch Extension/Info.plist" to 101
Updated CFBundleVersion in "Workout.xcodeproj/../Watch Intents/Info.plist" to 101
Updated CFBundleVersion in "Workout.xcodeproj/../Watch/Info.plist" to 101
Updated CFBundleVersion in "Workout.xcodeproj/../Workout/Info.plist" to 101
Updated CFBundleVersion in "Workout.xcodeproj/../WorkoutTests/Info.plist" to 101
Updated CFBundleVersion in "Workout.xcodeproj/../iOS Intents/Info.plist" to 101

SchemeのPre-actionsに設定すれば、ビルドする度にこのコマンドを実行することもできます。

Releaseビルドのときのみ実行したい場合は、if文を書けます。

if [ ${CONFIGURATION} == "Release" ]; then
    cd "${PROJECT_DIR}"
    agvtool next-version -all
fi

project.pbxproj ファイルと、各TARGETのInfo.plistが変更されるので、変更ファイルは結構多め。
更新された、Current Project Versionは残していく必要があるので、Gitの管理対象外にはできなさそう。
Info.plistはまだしも、project.pbxprojはただでさえコンフリクトすると面倒なファイルなのでツライ印象。
リリースビルドする時のみ更新する + その操作をする人は必ず1人 ならばアリかもしれない。

参考までに、Build Numberでなく、バージョン番号を一気に設定するには以下で。

agvtool new-marketing-version {バージョン番号}

2. Gitのリビジョン情報を使う

git rev-list HEAD | wc -l | tr -d ' '

GitのRevisionのリストの数をBuild Numberとして扱うパターンです。
行数をカウントして無駄なスペースを外しています。

逆にBuild Numberからリビジョンを取得は以下で。

git rev-list HEAD | sed -n {BuildNumberの数}p

数をインクリメントしていく方法より、こちらの方が好きなんですよね...。
数字に意味がある というのもあるんですが、複数の人が自由にビルドしているという環境だとこちらの方がより安心できそうな気がして。
で、以降はこのBuild Numberを反映する方法イロイロです。

a. Run ScriptでInfo.plistを更新する

まずは、一番最初に思いつく方法から。
Build Phases に Run Scriptを追加し、PlistBuddyをつかってInfo.plistを編集します。

BuildNumber=$(git rev-list HEAD | wc -l | tr -d ' ')
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BuildNumber" "$INFOPLIST_FILE"

複数のTARGETがある時は、SchemeのPre-actionsにまとめて書いた方がスッキリするかもです。
経験上、Extensionを追加する(TARGETが増える)アップデートをした時によく忘れます(笑)
まぁ一度設定してしまえばOKなので、良いのですが....各Info.plistが変更になってしまうのが気になります。

b. Preprocess Info.plist を利用する

Preprocess Info.plist を利用すると、Temporaryな中間ファイルに更新するので、Gitの変更対象外になるメリットがあります。

Build Settings で Packaging > Preprocess Info.plist File を YES にする。
複数のターゲットがあるので PROJECT で YES にしました。

次に各TARGETに、Run Scriptを追加しCopy Bundle Resourcesの前に移動。PlistBuddyを使って正しいBuild Numberを設定するように書きます。

更新対象ファイルを、{TEMP_DIR}/Preprocessed-Info.plist にするのがポイント。
このファイルがビルド中に生成されるTemporaryファイルになります。

これでうまくいくと良かったのですが、なぜかiOSのSiri ExtensionのTargetのもののみうまくいかず。
Run Script実行直後には更新できているのは確認したのですが、その後どこかで上書きされているようで....原因突き止められず。
iOS本体、WatchのExtension, WatchのSiri ExtensionのTARGETはうまく更新できているようだったので、ほとんどのプロジェクトは大丈夫な気がしますが、XCodeのバージョンや新しいExtensionなどで動きが変わるかもという不安は残る。

c. Info.plist Preprocessor でマクロ定義する

ヘッダーファイルににBuild Numberのマクロ定義を書いておいて、Info.plistでそれを参照することができます。
Build Settings > User-Defined で定義するのと少し近いイメージな割に遠回り感がありますが、project.pbxproj の変更ではない のと、Build Numberの定義を別ファイルにできるのが大きい!

今回は、InfoPlistPrefix.h というファイルをScriptから作成していきます。

Build Settings で Packaging > Preprocess Info.plist File を YES にする。
Info.plist Preprocessor Prefix File に、\${PROJECT_DIR}/InfoPlistPrefix.h を設定します。
全てのTARGETに有効になるようPROJECTにて設定。

各TARGETのInfo.plistのBundle version(Build Number)を、FLW_BUILD_NUMBER にします。

FLW_BUILD_NUMBER というのは適当な名前なので、自由に変えてOKです。

次に、InfoPlistPrefix.hファイルを自動作成するスクリプトを書きます。
SchemeのPre-actionsにRun Scriptを追加し、以下のように記述します。

cd "${PROJECT_DIR}"
GIT_REV=$(git rev-list HEAD | wc -l | tr -d ' ')
echo "#define FLW_BUILD_NUMBER "${GIT_REV}"\n" > "InfoPlistPrefix.h"
EOF

Provide build setting from の設定も忘れずに!

これでビルドすると、InfoPlistPrefix.hが作成され、以下のような内容になっているはず。

#define FLW_BUILD_NUMBER {現在のビルド番号}

そしてGitの管理対象外にするために、.gitignore に InfoPlistPrefix.h を追加。
これでArchiveすると各Info.plistのBuild Numberが更新されていると思います。

懸念点としては、Schemeを新しく作成した時の設定漏れですが、リリースビルドする時に使用するSchemeはまず決まっているだろう...ということで。

まとめ

あれやこれやと方法を模索しましたが、不安要素はできるだけ排除できる、明確な方法が一番いいと思う。
こういう所でハマってしまうのはツライですもんね。

CIを使っているのであれば、単純にビルドの前にInfo.plistを変更するスクリプトをかませるのがよさげ。
agvtoolを使ってもよいけど、PlistBuddyでポチポチ書くのも全然アリと思う。

CIを使わず、XCodeのビルドだけで更新したい場合、
TARGETがiOSのみなのであれば、a. Run ScriptでInfo.plistを更新する で十分そう。
TARGETが複数ある時は、c. Info.plist Preprocessor でマクロ定義する を試してみるのもよさげ。
...がいまのところの結論。