TestFlightへのアップロード部分の内容はAppleに買収されiTunes Connectに統合される前のTestFlightについての情報です、testflightapp.comは2015/2/26に終了です
コードを書き終わったがもう会社に行かなければならない、テストのために手持ちのデバイスにインストールしておきたい・・・。趣味でつくっているアプリこそ自動化して開発の時間を捻出すべきなのでは?そんなことを思っていたらMarvericksが無料でリリースされたので押入れで眠っていたMacBook AirにMarvericksとJenkinsを入れてリリース作業を自動化してみました。
前提として
- Jenkinsのセットアップは割愛しています。かわりに別の記事をあげているので参考にしてください。
- OS XにJenkinsをHomebrewでセットアップする
- Xcode5, Jenkins 1.542, CocoaPodsを使ったプロジェクトを使います
- Xcodeのプロジェクト名、ワークスペース名を MyProject としているので読み替えて使ってください
- 今回はビルドと配布に焦点をあてています、静的解析やテストに関する話はありません
- TestFlightへの自動アップロード、AppStoreに提出するipaファイルの自動ビルドまでを行います
ビルド環境のセットアップ
新しいマシンにセットアップする場合は以下を忘れないようにしておきます。
- Xcode
- Xcode Command Line Tools
- iPhone Developer Program証明書
- CocoaPods (使っている場合)
KeyChainへのアクセスなど初回に対話が必要な設定があるのでいきなりJenkinsでビルドするのではなく、一度プロジェクトを取得して、Xcodeでビルドができるか確認しておきましょう。
自動ビルドに必要なファイルのコミット
Provisioning Profile
Provisioning Profileを取得、更新しなくてよいようにリポジトリにコミットしておきます。Dropboxから参照するのもよいでしょう。ファイルは任意の場所にあってもビルド前に行うシェルスクリプトでプロジェクトディレクトリにコピーします。
スキーマ設定ファイル
xcodebuildでワークスペースファイルを使う際にスキーマファイルが必要になります。このファイルが Xcodeでワークスペースファイルを開いた際に生成される ものであるため、CocoaPodsで自動生成したワークスペースをxcodebuildでビルドしようとするとスキーマファイルが見つからずにビルドが失敗します。
MyProject.xcodeproj/xcuserdata/<UserName>.xcuserdatad/xcschemes/MyProject.xcscheme
MyProject.xcodeproj/xcuserdata/<UserName>.xcuserdatad/xcschemes/xcschememanagement.plist
この問題を回避するために事前にスキーマファイルをJenkinsを実行するユーザのファイルとしてコミットしておきます。
参考: JenkinsでiOSのビルドを行う時にハマるポイントとその解決法・1
使うJenkinsプラグイン
以下のものを使っています。
- Git Plugin
- Git Client Plugin
- Xcode Plugin
- Testflight Plugin
- Run Condition Plugin
- Conditional BuildStep Plugin
- Flexible Publish Plugin
- Next Build Number Plugin
Jenkins CLIを使うなら以下のようにしてインストールできます。
wget http://localhost:8080/jnlpJars/jenkins-cli.jar
java -jar jenkins-cli.jar -s http://localhost:8080 install-plugin \
git git-client next-build-number run-condition conditional-buildstep flexible-publish \
xcode-plugin testflight
java -jar jenkins-cli.jar -s http://localhost:8080 safe-restart
rubyやbundlerで細かく制御するためCocoaPodsプラグインは使っておらずシェルで以下を実行します。(余談を参照)
pod install
ビルドの種類
Jenkinsからは1つのジョブで以下の3種類のビルドを行います。
- Dailyビルド
- TestFlight配布用ビルドとアップロード (テスト用)
- AppStore提出用ビルド
1が静的チェックやテストが目的のジョブです。SCMをポーリングさせればコミットしていない日はジョブを実行しないことも可能です。
2と3は配布のためのジョブです。自分のタイミングで行うためにパラメータ付きビルドで手動で行うジョブにします。この2つはTestFlight SDKを組み込むかどうかと、Provisioning Profileを変えているため別々のビルドで成果物を作成します。
使うパラメータ
パラメータ付きビルドで使うパラメータは以下のとおりです。ジョブを作成したら登録してください。デフォルト値がオフなので毎日の自動ビルドでは配布に関する処理は何も行いません。
パラメータ名 | 型 | デフォルト値 | 説明 |
---|---|---|---|
TESTFLIGHT_UPLOAD | 真偽値 | false | TestFlightへアップロードするかどうか |
BUILD_FOR_APPSTORE | 真偽値 | false | AppStore配布用のビルドを行うかどうか |
パラメータ付きビルドで使ったパラメータの値はJenkinsのジョブの実行時に 環境変数 に設定されます。シェルなどから環境変数を参照すれば処理の分岐が可能になります。
ビルドスクリプトの抜粋
Jenkins Xcode Plugin の前処理
ビルド手順の追加 から シェルの実行 を追加し、以下のようなシェルの処理を Jenkins Xcode Plugin のビルドの 前 に行っています。ここで行うのは
- Provisioning Profileの切り替え
- TestFlight SDK の有効・無効
- CocoaPodsのインストール
です。
TestFlightでAdHocに配布する場合と、AppleStoreに配布する場合とでProvisioning Profileを分けているので配布方法によってファイルを切り替えています。Jenkins Xcode Plugin には MyProject.mobileprovision
といった適当なファイル名を設定しておき、その名前に元ファイルからコピーしています。
cp MyProject_Ad_Hoc_Distribution.mobileprovision MyProject.mobileprovision
if $BUILD_FOR_APPSTORE = true ; then
rake configure_appstore
cp MyProject_AppStore_Distribution.mobileprovision MyProject.mobileprovision
fi
pod install
rake configure_appstore
の処理ではTestFlight SDKを使わないようにソースファイルを変更しています。 Rakefileは以下のような内容になっています。
# encoding: utf-8
require 'rake'
task :configure_appstore do
build_content = File.read('MyProject/MyProjectBuild.h').gsub(/(USE_TESTFLIGHT_SDK) 1/, '\1 0')
open('MyProject/MyProjectBuild.h', 'w') do |f|
f.<< build_content
end
podfile_content = File.read('Podfile').gsub(/^pod 'TestFlightSDK'/, '#\0')
open('Podfile', 'w') do |f|
f.<< podfile_content
end
end
TestFlight SDKをCocoaPodsで利用している前提です。あるヘッダファイルとPodfileをそれぞれ以下のように置換しています。
#define USE_TESTFLIGHT_SDK 1
-> #define USE_TESTFLIGHT_SDK 0
pod 'TestFlightSDK'
-> #pod 'TestFlightSDK'
リポジトリにコミットしてあるソースコードはTestFlight SDKを組み込んだ状態になっているので configure_appstore タスクを実行した場合に TestFlight SDKを使うコードをコンパイルしないように変更します。
USE_TESTFLIGHT_SDK で処理を分岐する話は iOSでTestFlight SDKを使うときのメモ を参考にしてください。
Jenkins Xcode Plugin の設定
Xcodeビルドの設定に入ります。 ビルド手順の追加 から Xcode を追加します。
ビルドの設定
General build settings を設定します。配布が目的なのでビルドの構成である Configration に Release
を設定し、成果物として使う ipaファイルの名前は MyProject-${VERSION}-${BUILD_DATE}
としておきます。
次に Advanced Xcode build options を設定します。Jenkins Xcode Pluginがわかりにくいのがこの部分です。Xcodeの場合、普通にプロジェクトを作ると、指定したプロジェクト名でプロジェクトディレクトリ、プロジェクトファイル、ワークスペースファイル、ターゲットがつくられるので何をどこで設定しているのがわからなくなります。
参考までに設定しているプロジェクトの階層を以下に示します。ここではソースコードと他のファイルを管理するために、リポジトリの1階層下にプロジェクトのファイルがあります(#1)、リポジトリ直下にファイルがない場合は Xcode Project Directory にプロジェクトのあるパスを設定します。
CocoaPodsが作成したWorkspaceファイル (#2) を使うので Xcode Workspace File にファイル名を指定しています。
SYMROOTに ${WORKSPACE}/build
(#3) を設定しているのでここにipaファイルが出力されます。
.
├── MyProject # 1
│ ├── MyProject
│ ├── MyProject.xcodeproj
│ ├── MyProject.xcworkspace # 2
│ ├── Podfile
│ ├── Podfile.lock
│ └── Pods
├── build #3
│ └── Release-iphoneos
│ └── MyProject-NN-YYYY.mm.dd.ipa
└── README.md
署名
署名は Code signing & OS X keychain options 項目で行います。 Embedded Profile はビルド前の処理でコピーしておいたファイルを設定します。
homebrewでインストールした場合ユーザ権限でJenkinsを動かせるので Unlock Keychain を有効にして Keychain path に ${HOME}/Library/Keychains/login.keychain
と Keychain password にパスワードを入力しておけばユーザの証明書を参照できます。
ビルド番号の設定
Jenkins Xcode Pluginでは Versioning 項目にチェックをいれるとバージョン番号を自動で設定することができます。
Technical version に ${BUILD_NUMBER}
を設定することでJenkinsのビルド番号を CFBundleVersion に設定できます。 Marketing version(CFBundleShortVersionString)はリポジトリで管理してCFBundleVersionはJenkinsに任せるのが良いと思います。
Jenkinsのビルド番号は 1 から始まりますが、Next Build Number Plugin を使えば番号を飛ばすことができます。過去のビルドを削除するなどしてより大きな数値のビルドが存在しないようにすれば番号を戻すことも可能です。
Jenkins Xcode Plugin の後処理
ビルド手順の追加 から シェルの実行 をもう一つ追加し、以下のようなシェルの処理を Jenkins Xcode Plugin のビルドの 後 に行っています。
BUILD_DATE=`date '+%Y.%m.%d'`
cd build/Release-iphoneos
if $BUILD_FOR_APPSTORE = true ; then
mv MyProject-${BUILD_NUMBER}-${BUILD_DATE}.ipa MyProject-${BUILD_NUMBER}-${BUILD_DATE}_for_appstore.ipa
fi
BUILD_FOR_APPSTORE
が有効のときにファイル名のsuffixに _for_appstore
をつけています。これは間違えてTestFlightを組み込んだビルドをアップロードしてしまわないように人間がファイル名で判別しやすいようにするためです。
TestFlightへのアップロード設定
ここからはビルド後の処理の設定です。
TestFlightへのアップロードには Testflight Plugin を使います。Testflight Plugin をインストールするとビルド後の処理でTestFlightへのアップロード作業を追加できます。
アップロードにはTokenが必要になるので取得しておきます。
API Token: https://testflightapp.com/account/#api
TeamToken: https://testflightapp.com/dashboard/team/edit/
Tokenの設定はジョブではなく、 Jenkinsの管理 > システムの設定 > Test Flight で行います。チームごとに複数のTokenが設定できます。
条件付きでアップロード処理を実行させるため、 ビルド後の処理の追加 で Flexible publish を使い Token を ${ENV,var="TESTFLIGHT_UPLOAD"}
にし、 Action で Upload to TestFlight を選択します。
これにより手動によるパラメータ付きビルドで TESTFLIGHT_UPLOAD
をチェックした場合にのみ TestFlight へのUploadが行えます。
TestFlight Pluginでは高度な設定から配布時の設定が行えます。まずは Distribution Lists を使って自分宛に絞って送るようにしています。
Gitのコメントなどを Build Notes に設定することもできますが、コミットのコメントと知人などのテスターへ適切な説明を両立させるのはなかなか困難です。TestFlightではビルドの説明や配布するメンバーを後からでも設定できるのでまずは自分で試して問題ないことを確認してから、ビルドの説明を入力して、配布するメンバーを追加するという運用が良いと思います。
成果物の設定
ビルド後の処理の追加 で 成果物を保存 を追加し、 build/**/*.ipa,build/**/*dSYM.zip
を保存するようにしておきます。
これでビルドごとのipaファイルがJenkinsからダウンロードできるようになります。
ジョブの実行
ジョブの設定ができたらSCMをポーリングするなどして毎日自動のビルドを走らせます。静的チェックやユニットテストはここで行いましょう。
配布するときはJenkinsにログインして手動でビルドを実行します。手動でジョブを実行しようとするとパラメータの入力を求められるので、TestFlightにアプロードするときは TESTFLIGHT_UPLOAD
にチェックを入れ、AppStoreにリリースするときは BUILD_FOR_APPSTORE
にチェックを入れてビルドを実行します。TestFlight SDKを組みこんだものをAppStoreに提出したくないのでこの2つのパラメータは排他的に使います。
TestFlightへのアップロード
ジョブの設定が正しく動いていればTestFlightにアプロードは自動で行えます。実装が終わったらビルドボタンを押すだけでiOS端末でテストができるようになります。
AppStoreへの提出
パラメータ付きビルドで BUILD_FOR_APPSTORE
を有効にしてビルドを行うと、ipa ファイルがビルドの成果物として作成されるのでこのファイルをAppStoreに提出します。
ipaファイルを指定してアップロードする場合は Xcode (Organizer) ではなく Application Loader を使います。Xcode のメニューの Xcode > Open Developer Tool > Application Loader から起動できます。
Organizerを使う場合と同じく、iTunesConnectのアプリのVersionで Ready to Upload Binary を押した状態になっていればアップロードが行えます。
余談
- 本当は OS X Server + Bot に興味があったのだけど、CocoaPods周りが面倒そうだったので挫けました。でも諦めてはいません
- どうもiOSプロジェクトでAnt, MavenやGradleを使うことに違和感を覚え、Macにはrubyが入っているし、CocoaPodsもrubyが必要なのでシェルやrakeで済ませることにしました
- TestFlightでビルドを配布する上でTestFlightSDKは必須ではありません。今回はTestFlightSDKを使うビルド、使わないビルドをそれぞれ作っていますが、TestFlightSDKを使わないで同じビルドで署名だけをつけかえるだけという方法も有効です。
- 参考:
iOSアプリの署名を付け替える http://qiita.com/beakmark/items/33c0b73603e491f08a33 - Jenkins上で署名できるようになったので署名に関する設定をプロジェクトファイルから削除しました(リポジトリにコミットしているのがなんか嫌だった)
- いつもOrganizerを使ってAppStoreに提出していたので ipa ファイルを指定して提出する方法の調べ方がわからなかったのですが、Xcodeを使わないフレームワーク (AIR for iOSとか) 周辺のドキュメントを調べるとわかりました
- 今回の内容とは関係ないですが Ruby の実行に rbenv を使っていて CocoaPods のバージョンをプロジェクトによって固定するために bundler でプロジェクトローカルにインストールしています。実際には
pod install
のコマンド部分は以下のような処理を実行しています。
export PATH=$HOME/bin:$HOME/.rbenv/bin:/usr/local/bin:$PATH
eval "$(rbenv init -)"
rbenv shell 1.9.3-p392
bundle install --path=vendor/bundle
bundle exec pod install
- Jenkinsを長時間起動していると状態がおかしくなる問題に遭遇していて定期的に再起動させています。 https://issues.jenkins-ci.org/browse/JENKINS-17526
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>homebrew.mxcl.jenkins.restart</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-jar</string>
<string>/Users/Shared/jenkins-cli.jar</string>
<string>-s</string>
<string>http://localhost:8080</string>
<string>safe-restart</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>15</integer>
<key>Minute</key>
<integer>00</integer>
</dict>
</dict>
</plist>
wget http://localhost:8080/jnlpJars/jenkins-cli.jar /Users/Shared/jenkins-cli.jar
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.jenkins.restart.plist