iOS Advent Calender 2013 iOS second stage一日目担当の@keroxpです。普段は大学の研究室でインターフェースの研究をしていたり、仕事先の会社でiOSアプリの開発を行っていたりします。
さて、今回私が取り上げるテーマは「Xcodeと自動化」です。予告のタイトルでは「XcodeとContinuous Integration」だったのですが、CIを含めた自動化という題材でお送りしたいと思います。
自動化の意義
iOSに限らず、ソフトウェア開発の日常には様々なイライラが存在します。一つ一つは小さく、手間も少ないように思える事柄でも、手動でやらなくてはいけないことがひとつふたつ...と増えていったり、開発期間が長引くにつれて毎日毎日一定時間同じ作業を手動で行わなくてはならなくなります。
Don't Repeat Yourself!!
そこでこの記事ではXcodeを用いたiOSアプリケーションの開発で発生しがちな面倒な人力処理を自動化するためのTipsを実験的にまとめてみます。
ビルド設定の自動化
XcodeはIDEとしては優れている部類だと思いますが、それはビルドの複雑な処理をうまい具合に隠蔽しているからであって、ビルドのプロセス自体はとても複雑です。Xcodeのビルドオプションは設定項目が多すぎて何がどういうオプションなのかまったく把握できません。初期設定のままなら問題ありませんが、少し手を加えたりすると途端にビルドできなくなったりして困ります。
XcodeのBuild Settingの画面
そこでおすすめするのがxcconfigファイルです。xcconfigはXcode4から導入されたBuild Configurationのまとめ設定ファイルで、xcconfigファイルをプロジェクトにaddしておくと、プロジェクトのinfoタブから設定できます。Configurationを分ける必要があるのは主にDebugビルドとReleaseビルドですが、ONLY_ACTIVE_ARCHやCOPY_PHASE_STRP、ARCHSといった闇の深そうな設定をうまくまとめてくれているxcconfigファイルがあります。
https://github.com/jspahrsummers/xcconfigs
これはCocoa界の偉人Justin Spahr-Summers氏が公開しているファイルで、Mantleやoctokitなどのgithub謹製のプロジェクトで用いられている秘伝のconfigファイルです。ありがたく使わせてもらいましょう。このxcconfigファイルには他にもClang Static Analyzerの設定が入っており、かなりstrictな開発環境になります。
たとえば、このconfigurationをいれていると普通ならWarningになってビルドが成功する場面でもビルドできなくなります。こういった横着的な実装はあとあと良くないことを引き起こす危険性があるのでプレビルドの段階で弾いておくべきでしょう。
普通ならwarningで済む場面が...
エラーになる!
また、xcconfigファイルは#includeを使うことができるので、project specifiedな設定も上記のベースファイルと一緒に使うことができます。
パッケージ管理の自動化
iOS開発の現場では外部のライブラリの使用が普通ですが、その管理もできるだけ自動化したいところです。Cocoapodsは言わずと知れたCocoaのパッケージマネージャです。パッケージマネージャを使うことで、依存パッケージの一括管理とアップデートを自動化することができます。使い方は「Cocoapods 使い方」でググればいくらでも出てくると思うので、ここではCocoa Podsを使っているときに起こりがちな問題と、あまり知られていないPodfileの書き方を紹介します。
Podfileの書き方
一般的にはPodfileはこんな書き方をすると思います。
platform :iOS, "7.0"
pod "AFNetworking"
pod "BlocksKit"
この手軽さがCocoaPodsの持ち味なのですが、もっとプロジェクトが複雑になったときに困る場合があります。たとえば、アプリのiPhone版とiPad版でターゲットを分けた場合など、 プロジェクトの中に複数のターゲットが存在する ときです。CocoaPodsのソースによれば、CocoaPodsは明示的な宣言が無い限り、プロジェクトの先頭にあるターゲットをリンク先として選択します。
例えばこのようなターゲットがプロジェクトにある場合、上記のPodfileは先頭のXcodeCIというターゲットにリンクされますが、XcodeCI iPadというターゲットにはリンクされません。また、テストターゲットでのみKiwiなどのテスト用フレームワークを使いたい場合はどうすればいいでしょう? またインストール前にかならずCocoaPodsのアップデートを行いたいときは?...
Podfileでは上記の他に以下のようなDSLを用いることができます。
# プラットフォームの指定
platform :ios, "7.0"
# Xcodeプロジェクトまたはワークスペースの指定
xcodeproj "XcodeCI"
workspace "XcoeeCI"
# Podパッケージをリンクさせるターゲット
link_with "XodeCI", "XcodeCI iPad"
# 使用するパッケージとバージョンの宣言
pod "AFNetworking", "~> 2.0.0"
# インストール前に行われるアクション
pre_install do |installer_representation|
system "gem update --system"
system "gem update cocoapods"
p "install started"
end
# ターゲット別の使用パッケージの宣言
# :exclusiveオプションはtarget外で宣言されたpodパッケージを継承しないことを表す
target "XcodeCITests", :exclusive => true do
pod "Kiwi/XCTest"
end
# インストール後に行われるアクション
post_install do |installer_representation|
p "install finished!"
end
これで
pod
と実行すると
のようにセットアップが完了します。
また、pre_installのブロックでgemのアップデートを実行していますが、systemはrubyでコマンドを実行するためのKernelオブジェクトのメソッドです。つまり、 Podfileはrubyファイルなのです。 なので、Podfileの中には他にも任意のrubyスクリプト+コマンドを書くことができます。
Podfile.lockってなんなの?
Podfileを作成し、
pod install
したあとに生成されるPodfile.lock。なんかよく分からないけど怪しそうだから触らないことが多いと思いますが、実はこのファイルとても重要なのです。中身を見てみると、なにやらPodfileに書いてあることとあまり変わらないことが書いてあるように見えますが、重要な点がふたつ。
ひとつはパッケージのバージョン。Podfileにパッケージのバージョンが指定されていなかった場合、最初にインストールされた際のバージョンがここに保存されているのです。なので、別の環境でpod installしてもきちんと同じバージョンが入るんですね。
ふたつめはcocoapods gemのバージョン。これは最後にpodを使った際(詳しくはわからないですがinstall, updateとか)の環境のcocoapodsのバージョンが記されています。なので、複数のマシンで開発をしている場合に、誰かがいじって別の人がpod installしようとしたとき、ローカルのcocoapodsのバージョンが違った場合、コンソールにwarningが出るはずです。なので、warningがでたら
gem update cocoapods
として最新バージョンを使うようにしましょう。
このようにPodfile.lockはとても大事なファイルなので、 必ずgit treeにいれておきましょう。
Xcodeのカスタムビルドアクション
Xcodeにはビルド時にシェルスクリプトのカスタムアクションをいれることができます。
例えば、Debugビルド時以外のビルドの場合のみ、ビルド番号を上げたいときは以下のようなシェルスクリプトをBuild PhaseからEditor-Add Build Phase-Add Run Sctipt Build Phaseから追加することで実現可能です。
地味ですがとても便利な機能です。他にも良い使い方をご存じの方がいれば教えてください。
GitHub + Travis CIを用いたContinuous Integration
Continuous Integrationの詳しい説明はhttp://en.wikipedia.org/wiki/Continuous_integrationを見るといいと思います。iOSにおいてCIはあまり一般的ではなく浸透していませんが、webサービスなどでは頻繁なアップデートと安定性の両立という点において重要な位置づけになっています。Xcode5ではOSX Serverと連携してCIが行えるようになりましたが、OSX Serverが普及していないのであまり流行っていません。そこでおすすめしたいのがTravis CIです。githubとの連携が必須ですが、無料でセットアップも必要ないのでおすすめです。(※Travis CIはつい最近Xcode5でのビルドができるようになりました。
Xcode 5 and iOS 7 now available for Mac and iOS Builds!)
クライアントサイドアプリケーションにおけるテストとCIの意義は個人的には必須だとは思っていないのですが、(UIと密接に関係するクライアントサイドはTry&Errorでテストするしかない部分が多いため)UIと完全に分離したデータモデルやコントローラのクラスではできるだけ作るようにしたほうがいいと思います。CIを行う理由はいくつかありますが、そのひとつに テストの数が多い、非同期処理がいくつもある、などの理由でテストに時間がかかる場合 があげられると思います。
また、ビルド環境の違いを吸収するという目的もあると思います。チームで開発を行う場合ツールやバージョンを合わせるのが一般的ですが、何かの違いで依存のある変更がgitにコミットされていない場合などに、その起こりうる人的ミスを早期に発見することができます。(例:Podfileを作成したがgit treeにいれずにcommit pushしてしまった、など)
というけでCIのためのセットアップをしてみましょう。
基本的な設定に関してはこちらの記事などを参考にするといいと思います。
iOSのライブラリだってTravis CIとかCoverallsとか使うべき
CIは基本的にマシンが自動的に行うため、Xcodeなどを操作することはできません。そこで用いられるのがコマンドラインでのビルドツールです。Xcodeでビルドを行う場合⌘+Bが普通ですが、Command Line Toolでインストールされるxcodebuildを用いることでもビルドができます。
xcodebuild -project XcodeCI.xcodeproj -scheme XcodeCI
##xctool
https://github.com/facebook/xctool
xctoolはfacebookが開発したxcodebuildのようなツールで、基本的な機能は同じながらテストの場面で便利な機能があるのでおすすめです。
インストール
brew install xctool
使い方
xctool -project XcodeCI.xcodeproj -scheme XcodeCI build
Xcodeのschemeは以下のようにManage Schemesからsharedにチェックを入れないと他の環境でビルドすることができないので設定しておきましょう。
##Makefileと.travis.ymlを作る
Travis CIがビルドとテストを行うためのファイルを作ります。
今回の場合、テストが行われるマシンは普通のMacなので、スクリプトであれば何でも構わないのですがMakefileが一番簡単だと思います。
PROJECT=XcodeCI.xcodeproj
SCHEME=XcodeCI
defualt: clean build test
clean:
xctool \
-project ${PROJECT} \
-scheme ${SCHEME} \
clean
build:
xctool \
-project ${PROJECT} \
-scheme ${SCHEME} \
build \
-sdk iphonesimulator
test:
xctool \
-project ${PROJECT} \
-scheme ${SCHEME} \
test \
-test-sdk iphonesimulator \
-parallelize
language: objective-c
script:
- make
##Rakefileを使う
さて、小規模なプロジェクトならば上記のようなMakefileを作るだけでいいのですが、場合によっては何らかの事情でプロジェクトの中に結構な数のターゲットや別々のテストユニットが混在する場合があります。そんなとき、すべてのターゲットをビルドしてテストしたい場合、xctoolではすべてのスキームを選択することはできず、xcodebuildの-alltargetsオプションもworkspaceを使っている場合は使えません。
all:
xctool -workspace ${WORKSPACE} -scheme ${SCHEME1} build
xctool -workspace ${WORKSPACE} -scheme ${SCHEME2} build
xctool -workspace ${WORKSPACE} -scheme ${SCHEME3} build
.
.
.
その上、毎回-workspaceなどの変わらないオプションを付けるのも煩雑です。そこで、Rakefileの登場です。rakeはRubyで記述されたMakefileで、Makefileと異なりより柔軟なタスクの記述ができます。たとえばXcodeCI.xcworkspaceのXcodeCIというスキームをDebug Configurationでビルドしたいとき
rake build:XcodeCI:Debug
のようにビルドできたらスマートだと思います。
はい、そこでこんなRakefileを書きました。(長すぎるのでgistに避難)
https://gist.github.com/keroxp/7707253
これをxcodeprojやwcworkspaceと同じディレクトリにいれて以下のようなRakefileをつくって
import "farm.rake"
# デフォルトのタスクを記述
task :default => ["clean","build:all","test:all"]
# 必要があればプロジェクトとワークスペースのパス
# $PROJECT = "Hoge.xcodeproj"
# $WORKSPACE = "Hoge.workspace"
# デフォルトのビルドスキーム
$PRIMARY_SCHEME = "XcodeCI"
rake
と打つとあら不思議。
プロジェクト中のすべてのスキームをビルドしてテストを行ってくれます。
これはなにをやってるかというと、上記のfarm.rakeというRakefileのなかでxcodeproj, xcworkspaceが持っている情報を読み込んで、スキームとターゲット、コンフィギュレーションに応じて動的にrakeタスクを作っているんですね。なので、例えばXcodeCIとXcodeCI iPadというスキームがあるプロジェクトの場合
rake info
と打つと
また、
rake --tasks
と打つと、現在実行可能なタスクの一覧が表示されます
これでコマンドラインでのビルドが簡単に記述できるようになりました!
※farm.rakeはcocoapods gemに依存しているので動かない人は
gem install cocoapods
をお願いします。
##.travis.ymlを書く
最後にTravis CIが実行するタスクを記述しましょう。
language: objective-c
before_install:
- gem install cocoapods
script:
- rake
これだけです。そしてあとはgithubにpushするだけです。
おわりに
いかがだったでしょうか? 初日にもかかわらずあまりiOSとは関係の無いTIPSになってしまいましたが、明日以降素晴らしい記事がぞくぞくと上げられるので大丈夫でしょう:)
お疲れさまでした!
今回使ったサンプルプロジェクトはhttps://github.com/keroxp/XcodeCIにアップしておいたのでよければ確認して下さい。