Build 番号の自動更新スクリプトで Info.plist そのものに書き込まないための考察
SPECIAL
Build 番号をスクリプトで自動更新するときに Info.plist そのものの更新をさけるためには …
iOS アプリでビルドの度に Build 番号を更新する方法と言えば、PlistBuddy コマンドを使ってプロジェクト内の Info.plist を書き換える方法が一般的に知られています。
その方法については iPhone アプリのビルド番号を自動的に更新する や「」の "12.4.1 ビルド番号を自動で増加させるスクリプトを登録する" の中で触れているので、実装の雰囲気についてはそちらをご覧ください。
ツイッターから届いてきた課題
自分は PlistBuddy を知って以来、ずっとそれだけを使ってきたのですが、先日にふとツイッターから、興味深い情報が舞い込んできました。
agvtoolとBuild Settings/Versioningのお話の発表資料をここにアップします(まだアップロード中) #potatotips https://t.co/0ZzUtWh8Fk
— 所 友太 (@tokorom) 2014, 5月 15
@tokorom 「さらば#Ifdef DEBUG」のところ、細かいツッコミなんですけど、Info.plistも変数を使えば、ビルド設定ごとにバージョンにPrefixつけたり可能ですよ。(つまり同じことできる)
— kishikawa katsumi (@k_katsumi) 2014, 5月 15
この話題の発端にあるのは agvtool という話題でした。
この agvtool というコマンド、自分もつい最近に存在を知る機会があって、その内容はぜんぜん知らなかったものだから、このスライドやツイートの流れを興味深く追わせて頂いてました。
そうしていたところ、話はまた極めて興味深い方向へと展開して行ったのでした。
@tokorom そうですね。これは余談なんですけどInfo.plistはINFOPLIST_PREPROCESSOR_DEFをtrueにするとInfo.plist内で#ifdefの分岐もできます。変数で足りるしXMLとしてinvalidになるのであまり使い所はないですが。
— kishikawa katsumi (@k_katsumi) 2014, 5月 15
これを拝見して、以前に岸川さんのブログ「コマンドライン引数(Launch arguments)は思ったより簡単に使える - 24/7 twenty-four seven」を見たときにも同じ感動を覚えたのは記憶に新しいです。
どのようにしたら日常では到底到達し得ないような深いところまでご存じなのだろうと、まさに感心してしまいます。
そして話は、次のテーマへと移り行きます。
@k_katsumi おー、知らなかったです!あざーす!今回、ビルド番号をビルド時に変更して、かつ、git diffで差分を出さない方法を模索してるのですが、じつはいいソリューションをご存知だったりしますか?
— 所 友太 (@tokorom) 2014, 5月 15
@tokorom 良いかどうかはともかく、できそうな方法は心当たりがあるので試してみます。
普段はリリース時はrake bump...みたいにマニュアルで上げていて、ベータ版はCIがビルドごとにコミットハッシュ付けてくれる方式でやってます。
— kishikawa katsumi (@k_katsumi) 2014, 5月 15
模索のはじまり
それからほどなくして、岸川さんからひとつの解が示されました。
@tokorom ちょっと準備がややこしいですが、こういう手もあるということで https://t.co/EQYS2gVrwm こんなのどうでしょうか。正直準備がめんどうなので僕はたぶん使わないですけど。
— kishikawa katsumi (@k_katsumi) 2014, 5月 15
普段だったら使わないような細部まで知っていて、要望に対して解を形にして提案できる、そういうのってカッコいいですよね。
ひきかえ自分はどうも文章力が弱いみたいで、相手の要望を満たせなかったり、自分の回答を適切に届けられないと我ながら感じることがしょっちゅうで、そんなところにも感動します。
そんな提案の内容を読み進めて行くと、段取りはいくらか必要なものの順序良く組み立てられていて、これはこれで自分には分かりやすい印象でした。そしてこの提案のおかげで、所さんの目的をようやくちゃんと汲み取ることができた気がします。
執筆時の記憶と照らして
そして岸川さんの挙げる「問題点」のところを読んだとき、そういえば自分も Build 番号の更新を Run Script で実行したときに "間に合わない" が理由で妥協策を取ったのを思い出しました。
というのも、最近に書き上げた書籍「」に掲載する "12.4.1 ビルド番号を自動で増加させるスクリプトを登録する" の原稿を書くための検証で、Build Phases で Run Script を実行して Build 番号を更新しても、その新しい番号がバンドルに間に合わないという場面に出合っていたのでした。
具体的には、次のようなスクリプトです。
infoPlistFile="${SRCROOT}/${INFOPLIST_FILE}"
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${infoPlistFile}")
buildNumber=$((${buildNumber%%.*} + 1))
/usr/libexec/PlistBuddy -c "Set:CFBundleVersion $buildNumber" "${infoPlistFile}"
そのときは、このスクリプトが Build Phases のいちばん最初に実行されるように登録してみていました。
今であれば理屈的にも実際にも問題なく Info.plist の Build 番号を更新して、更新された Info.plist をバンドルに含める前に更新できるはずなのですけど、その検証のときはなぜかダメで、Build Phases のいちばん最初に実行しても更新が間に合いませんでした。
そのときにビルドログで確認する限りは、たしかに Run Script よりも先に Info.plist が何か処理されていたんですよね。それで間に合わないことを確信した「はず」なのですけれど、今だとどう見ても間に合ってくれそうです。
ちゃんと証拠を残していなくて、当時なぜ間に合わなかったかを確認する術はなさそうですが…。
ともあれそんな状況だったので、もっとスマートな方法はないか探したところ、バンドル内に配置された Info.plist (${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}/Info.plist) を直接更新することで、最新の Build 番号をバンドルに間に合わせる方法を見つけました。
でも、このときは元々の Info.plist 自体も更新したかったので、それだと元の Info.plist とバンドル内に配置された後の Info.plist の両方を書き換えなくてはいけないために不恰好、結局はひとつ前に更新した Build 番号を使うことで妥協しました。
実機での Debug 実行で浮上した問題点
そんなことがあったため、岸川さんが挙げた問題点を読んだとき、それなら岸川さんの BuildNumber.h のアイデアを使って、その内容をそのままバンドル内の Info.plist に書き込んだら上手くいくんじゃないかと思い立ちました。
そしてそれを伝える前に検証してみたところ、iOS シミュレータでの実行なら上手く行くところ、少なくとも自分の環境では iOS デバイス実機で実行すると、次のエラーが 2 回に 1 回の頻度で発生することが確認されました。
この状況では、もうひとつの解としては、お伝えできそうにもありませんね。
ちなみに後で伺った話では、所さんの環境ではこの問題は発生しないそうですけれど。
Info.plist のプリプロセスってどう動く?
ところで、岸川さんのおっしゃっていた "Preprocess Info.plist File" というのは "yes" にするとどういう振る舞いを見せるのでしょう。
それによっては新しい解が見つかりそうな気がしたため、確認してみることにしました。
ちなみに今回は、ビルドフェイスは次の順番で構成されています。
- Target Dependencies (0 items)
- Compile Sources (3 items)
- Link Binary With Libraries (3 items)
- Copy Bundle Resources (3 items)
ビルドフェイズの並び順は iOS アプリの Single View Application 用プロジェクトを作成したときから動かしていません。
ちなみにこのうち、最初の "Target Dependencies" は移動不可、他のフェイズは自由に移動できます。
Preprocess Info.plist File が "No" の場合
このとき、まずは通常の Info.plist をプリプロセスしないときのビルドの動きを確認してみました。
そうしたところ、さまざまなファイルがコンパイルされた後あたりで Info.plist" ファイルを処理したとみられる記録が見つかりました。
その直後にデバッグシンボルの作成やパッケージングの処理が行われることが見てとれます。
もう少し具体的に見ると、"Process EzSample_RewritePlist-Info.plist" の処理を含む "Copy EzSample_RewritePlist/en.lproj/InfoPlist.strings" から "Generate EzSample_RewritePlist.app.dSYM" までが "Copy Bundle Resources" ビルドフェイズに当たるようでした。
ちなみにそれより前に登場する『プリコンパイル済みヘッダーをコンパイル』する "Precompile EzSample_RewritePlist/EzSample_RewritePlist-Prefix.pch" のログのところから "Compile Sources" ビルドフェイズが実行されています。この直前が、可能な限りでもっとも早い段階でスクリプトを実行できるタイミングになります。
それより前の "Copy ResourceRules.plist" よりも先にスクリプトを実行できるタイミングはなさそうです。
ところで余談になりますけど、この "ResourceRules.plist" なのですが、この中で "Info.plist" と "ResourceRules.plist" という Dictionary に対してそれぞれ "omit" (Boolean) と "weight" (Number) を設定できそうなことまでは分かっています。
これって何に使う設定ファイルなのでしょうね。
Preprocess Info.plist File が "Yes" の場合
これを Info.plist をプリプロセスするように設定した場合だと、ログは次のように変化しました。
Info.plist に関係するログが 2 箇所に増えています。
何よりも先に処理されるタイミングで
最初に登場するのは『プリコンパイル済みヘッダーをコンパイル』するタイミングよりも前で、この行の詳細を見るとわかるのですが、このタイミングでプロジェクト内にある Info.plist ファイルを使って、$(TEMP_DIR) が指し示すフォルダーと同じ場所に、"Preprocessed-Info.plist" というファイルを出力しようとしていることが見て取れます。
プリコンパイル済みヘッダーをコンパイルするよりも前ということは、どんなに早く Run Script ビルドフェイズを実行しても「間に合わない」ことを意味しています。
Copy Bundle Resources ビルドフェイズのタイミングで
そして次に登場するログを見てみると、これは Info.plist をプリプロセスしなかったときの本来の処理と同じ場所に登場していることが分かります。これは先ほど説明した通り、"Copy Bundle Resources" ビルドフェイズの中のタイミングです。
そして、この行の詳細を確認してみると、プリプロセスを通さない設定だったときには「プロジェクト内の Info.plist」がバンドル内に出力される様子を窺わせる内容だったのが、プリプロセスを通す設定にしたときには「Preprocessed-Info.plist」が出力されるよう、対象の Info.plist が切り替わっている印象でした。
Info.plist プリプロセスの動きを整理する
この様子から Info.plist をプリプロセスしたときの動きを整理すると、次のような感じでした。
- "Preprocess Info.plist File" に "Yes" を設定すると、プロジェクト内の Info.plist をプリプロセスする処理が追加される。
- Info.plist をプリプロセスする処理は、他のどの Build Phases よりも早い段階で行われる。
- プリプロセスされた Info.plist ファイルは "$(TEMP_DIR)/Preprocessed-Info.plist" に保存される。($TEMP_DIR は事実から推定)
- Preprocessed-Info.plist は、プリプロセスを有効化していないときの、本来の Info.plist の処理と同じタイミングでバンドルに取り込まれる。
- Info.plist をプリプロセスした後から、バンドルに取り込むまでの間に、別のフェイズでどちらの Info.plist も扱われることはない。
このとき、プロジェクト内の Info.plist と Preprocessed-Info.plist の差分を取ってみると何も違いが見られなかったため、"Preprocess Info.plist File" に "Yes" を設定するだけにしておけば、元の Info.plist の内容と同じ Preprocessed-Info.plist が生成されると思っておいて良さそうです。
Preprocessed-Info.plist は書き換え可能
プリプロセスをとおして生成された Preprocessed-Info.plist は PlistBuddy コマンドを使って書き換え可能です。
つまり "Preprocess Info.plist File" を "Yes" に設定することで、元の Info.plist を書き換えることなく、Build Phases のもっとも早いタイミングから "Copy Bundle Resources" ビルドフェイズが実行される直前までの間、バンドルに含める Info.plist の内容を(Preprocessed-Info.plist を書き換えることで)変更する「猶予」を「安全に」得ることができると考えられます。
上で「猶予」や「安全に」という言葉を敢えて選んだのは、ビルドフェイズ内では「プロジェクト内の Info.plist」か「プリプロセス済みの Info.plist」かの違いがあるだけで、プログラマーが干渉できる Build Phases の中で、窺える限りはまったく等価に扱われているためです。
このような動きなら「想定外」の動きを見せることは、そうそうないことでしょう。
心配があるとすれば、事実から推定したプリプロセス済み Info.plist の保存場所くらいです。保存場所は、自分は "$TEMP_DIR" だろうと考えましたが、他にも同じ場所を示す環境変数として "$TEMP_FILES_DIR" や "$TEMP_FILE_DIR" があります。または、もしかすると別の環境変数の組み合わせかもしれません。
元の Info.plist を触らずにバンドルの Info.plist を書き換える、もうひとつの解(案)
以上の調査を踏まえて、プロジェクト内の Info.plist を更新せずにバンドル内の Info.plist を更新する方法として、次の解にたどり着きました。
Build Settings で "Preprocess Info.plist File" を "Yes" に設定して、"Copy Bundle Resources" ビルドフェイズを実行する前に "$(TEMP_DIR)/Preprocessed-Info.plist" を PlistBuddy で書き換える。
設定方法はシンプルで、まずはプロジェクトまたはターゲットの Build Settings 設定で Packaging カテゴリにある "Preprocess Info.plist File" を "Yes" に設定します。
そして、ターゲットの Build Phases 設定で "Copy Bundle Resources" ビルドフェイズを実行するまでの間に Info.plist を更新するスクリプトを記載します。
今回の例のような Build 番号を順次更新して行くだけのスクリプトの場合は、読み書きする対象の Info.plist を、オリジナルの "${SRCROOT}/${INFOPLIST_FILE}" から、プリプロセス後の "${TEMP_DIR}/Preprocessed-Info.plist" に変更するだけで簡単に対応できます。
infoPlistFile="${TEMP_DIR}/Preprocessed-Info.plist"
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${infoPlistFile}")
buildNumber=$((${buildNumber%%.*} + 1))
/usr/libexec/PlistBuddy -c "Set:CFBundleVersion $buildNumber" "${infoPlistFile}"
このように Preprocessed-Info.plist を書き換える方法を採ると、岸川さんが解説されていたような Info.plist を touch で更新するという対策も採らなくて大丈夫なようです。
これはおそらく、プリプロセス自体は Info.plist がされていないと行われないものの、その後で Preprocessed-Info.plist がスクリプトによって更新されるため、最後のバンドルに組み込む処理のところでは、Info.plist が(Preprocessed-Info.plist が)更新されたものと認識して組み込んでくれるためと想像できます。
Info.plist の値にサフィックスなどの文字を追加する場合
所さんが目的にされていた「Build 番号の情報に、"dev-" というプレフィックスと、コミット日時のサフィックスを追加する」場合だと、上記のスクリプトだけでは期待どおりに実現しない様子です。
その理由は、所さんが「これがXcodeでのバージョニングの決定版になるかも - TOKOROM BLOG」でご指摘されたとおり、Info.plist を更新しないと、前回の Preprocessed-Info.plist の内容が使われるため、プレフィックスとサフィックスが重複して付けられるという状況が発生します。
これを回避する方法として「最後に Run Script を追加して "${TEMP_DIR}/Preprocessed-Info.plist" を削除する」という方法も良いアイデアとは思うのですが、たとえば "$infoPlist" 変数にプリプロセス済みの Info.plist を設定していて "rm $infoPlist" して対応した場合、うっかり $infoPlist 変数の値をプロジェクト内の Info.plist のパスに変更して悲劇が生まれそうな怖さもあります。
もし元の Info.plist の内容にプレフィックスやサフィックスを付けることが目的であれば、それよりは次のようにして、元の Info.plist の内容を読み込んで、プリプロセス済みの Info.plist に書き込むという方法を採ると安心かもしれません。
infoPlistFileSource="${SRCROOT}/${INFOPLIST_FILE}"
infoPlistFileDestination="${TEMP_DIR}/Preprocessed-Info.plist"
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${infoPlistFileSource}")
buildNumber="${PREFIX}${buildNumber}${SUFFIX}"
/usr/libexec/PlistBuddy -c "Set:CFBundleVersion $buildNumber" "${infoPlistFileDestination}"
このようにすることで、プロジェクト内の Info.plist の内容を汚すことなく、バンドルのビルド番号にプレフィックスやサフィックスを常に適切に加えられます。
この方法なら、次のビルドのために Preprocessed-Info.plist を削除するための Run Script を登録する準備の手間も省けます。
調査の中で見かけた不明瞭な動作について
バンドル内の Info.plist をダイレクトに書き換えることの安全性は不明
バンドル内の Info.plist をダイレクトに書き換える方法は、少なくとも自分の環境では軽微な動作不良(2 回に 1 回、アプリを起動できない)が発生しているため、自分としては警戒してしまうところです。ログから確かな原因が分かれば、この方法の是非が判断できるところですけど、今のところは手がかりなしです。
本格的なパッケージングやコード署名はバンドル内を書き換えた後なので、安易な想像では、それほどの影響はなさそうにも思えるんですよね。
ダイレクトに書くことによる流れの変化と言えば、ログを見る限りでは、本来は Info.plist の処理が済んでからデバッグシンボルを生成するところ、ダイレクトの書き込みでは Info.plist を処理してデバッグシンボルが生成された後で Info.plist を書き換えるといったところでしょうか。
あと気になるところとすれば、本来の流れで Info.plist を処理するときに、"builtin-infoPlistUtility" というコマンドにたくさんの引数を渡して何かをしているところでしょうか。ここでもしかすると Info.plist に依存する何かを生成している可能性もないとは言えません。
とにかくこの辺りの動きが理解できないうちは、自分の環境で見れば、安心して使えないのが現状です。逆に Preprocessed-Info.plist を "Copy Bundle Resources" ビルドフェイズより前に書き換える限りでは、少なくとも自分の目が利く限りでは、安全性を期待しても大丈夫かなと思える印象でした。
Preprocessed-Info.plist の内容を Copy Bundle Resources ビルドフェイズの後に書き換えた場合
ところで "Copy Bundle Resources" ビルドフェイズよりも後に Preprocessed-Info.plist を書き換える Run Script を走らせた場合についてなのですが、アプリのバンドル内に Info.plist が配備されるのが "Copy Bundle Resources" のときのようなので、更新がビルドに間に合わないことは明らかなのですが、それに加えてひとつ気になる動きが見て取れました。
ビルドに更新が間に合わないだけなら、ひとつ遅れて更新内容がバンドルに反映されそうなものですが、ビルド番号をひとつずつ増加させるスクリプトを試してみたところ、2 回に 1 回の間隔で、ひとつ前の更新内容が反映される様子でした。間の 1 回のタイミングでは、更新されずに実行される様子です。
つまり、ビルド番号の進み方は「1」「1」「3」「3」… といった更新のされ方です。
ちなみに最初の「1」は、スクリプトでは「2」にしたものの、更新が間に合わなかったため「1」になっています。その次の「1」は、ビルド番号を更新するスクリプトで確認する限りでは Preprocessed-Info.plist の値は「3」なのですが、なぜバンドルから取得できる値が「1」になるのかは分かりません。
そして、その次に実行すると「3」になります。更新スクリプトでは「4」なのですが、それがバンドルに間に合わないため「3」になるようです。
このように "Copy Bundle Resources" ビルドフェイズから遅れてプリプロセス済み Info.plist を更新したときに限って「2 回に 1 回」というタイミングで不思議な動作を見せます。
このタイミング、バンドル内の Info.plist を書き換えたときに iOS デバイスで実行するときに出たメッセージのタイミングと同じですが、今回のビルド番号の問題は iOS シミュレータでも発生します。自分の環境で発生していた問題はコード署名の問題なので、もしかするとプリプロセス済みの Info.plist の更新が間に合わなかったときのこの動きと、バンドル内の Info.plist を直接書き換えた場合の iOS デバイスでの問題と、何か関係がないとも言い切れません。
念のため、プロジェクト内の Info.plist とプリプロセス済みの Info.plist とで更新日を確認してみましたが、プロジェクト内の Info.plist は書き換えないので常に同じ日付のまま、プリプロセス済みの Info.plist は毎回常に書き換えるので、その瞬間の日付に常に書き換わっていました。
別に 2 回に 1 回のタイミングで、日付の更新パターンが変わることはないようです。
最後の Build Phases の後に実行される touch について
もうひとつ、なんとなく目に留まったのが、すべての Build Phases の後に実行される touch でした。
この touch は、完全に出来上がったアプリパッケージに対して実行されていて、察するに、すべての処理が終わった後にアプリを touch することで、どんな中間ファイルよりも最新の日付に更新することが目的ではないかと思われます。
ちなみに Touch で終わるのは iOS シミュレータ用のビルドの場合で、iOS デバイス用のビルドの場合は、それに続けて ProcessProductPackaging という処理と、アプリに対する CodeSigning の処理が実行される様子です。
これらの処理が何かに影響するとは限りませんが、せっかく目に留まったので、念のため記載しておくことにします。