• 435
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

iOS Advend 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の画面

configuration.png

そこでおすすめするのが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で済む場面が...

analyze1.png

エラーになる!

analyze2.png

また、xcconfigファイルは#includeを使うことができるので、project specifiedな設定も上記のベースファイルと一緒に使うことができます。

パッケージ管理の自動化

cocoapods1.png

iOS開発の現場では外部のライブラリの使用が普通ですが、その管理もできるだけ自動化したいところです。Cocoapodsは言わずと知れたCocoaのパッケージマネージャです。パッケージマネージャを使うことで、依存パッケージの一括管理とアップデートを自動化することができます。使い方は「Cocoapods 使い方」でググればいくらでも出てくると思うので、ここではCocoa Podsを使っているときに起こりがちな問題と、あまり知られていないPodfileの書き方を紹介します。

Podfileの書き方

一般的にはPodfileはこんな書き方をすると思います。

Podfile
platform :iOS, "7.0"
pod "AFNetworking"
pod "BlocksKit"

この手軽さがCocoaPodsの持ち味なのですが、もっとプロジェクトが複雑になったときに困る場合があります。たとえば、アプリのiPhone版とiPad版でターゲットを分けた場合など、 プロジェクトの中に複数のターゲットが存在する ときです。CocoaPodsのソースによれば、CocoaPodsは明示的な宣言が無い限り、プロジェクトの先頭にあるターゲットをリンク先として選択します。

targets.png

例えばこのようなターゲットがプロジェクトにある場合、上記のPodfileは先頭のXcodeCIというターゲットにリンクされますが、XcodeCI iPadというターゲットにはリンクされません。また、テストターゲットでのみKiwiなどのテスト用フレームワークを使いたい場合はどうすればいいでしょう? またインストール前にかならずCocoaPodsのアップデートを行いたいときは?...

Podfileでは上記の他に以下のようなDSLを用いることができます。

Podfile
# プラットフォームの指定
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

これで
zsh
pod

と実行すると

pod.png

のようにセットアップが完了します。
また、pre_installのブロックでgemのアップデートを実行していますが、systemはrubyでコマンドを実行するためのKernelオブジェクトのメソッドです。つまり、 Podfileはrubyファイルなのです。 なので、Podfileの中には他にも任意のrubyスクリプト+コマンドを書くことができます。

Podfile.lockってなんなの?

Podfileを作成し、

pod install

したあとに生成されるPodfile.lock。なんかよく分からないけど怪しそうだから触らないことが多いと思いますが、実はこのファイルとても重要なのです。中身を見てみると、なにやらPodfileに書いてあることとあまり変わらないことが書いてあるように見えますが、重要な点がふたつ。

podfilelock.png

ひとつはパッケージのバージョン。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から追加することで実現可能です。

buildnumber.png

地味ですがとても便利な機能です。他にも良い使い方をご存じの方がいれば教えてください。

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にチェックを入れないと他の環境でビルドすることができないので設定しておきましょう。

schemes.png

Makefileと.travis.ymlを作る

Travis CIがビルドとテストを行うためのファイルを作ります。
今回の場合、テストが行われるマシンは普通のMacなので、スクリプトであれば何でも構わないのですがMakefileが一番簡単だと思います。

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
.travis.yml
language: objective-c

script:
  - make

Rakefileを使う

さて、小規模なプロジェクトならば上記のようなMakefileを作るだけでいいのですが、場合によっては何らかの事情でプロジェクトの中に結構な数のターゲットや別々のテストユニットが混在する場合があります。そんなとき、すべてのターゲットをビルドしてテストしたい場合、xctoolではすべてのスキームを選択することはできず、xcodebuildの-alltargetsオプションもworkspaceを使っている場合は使えません。

Makefile

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をつくって

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

と打つと

info.png
このような情報が表示されます。

また、

rake --tasks

と打つと、現在実行可能なタスクの一覧が表示されます

tasks.png

これでコマンドラインでのビルドが簡単に記述できるようになりました!

※farm.rakeはcocoapods gemに依存しているので動かない人は
zsh
gem install cocoapods

をお願いします。

.travis.ymlを書く

最後にTravis CIが実行するタスクを記述しましょう。

.travis.yml
language: objective-c

before_install:
  - gem install cocoapods
script:
  - rake

これだけです。そしてあとはgithubにpushするだけです。

おわりに

いかがだったでしょうか? 初日にもかかわらずあまりiOSとは関係の無いTIPSになってしまいましたが、明日以降素晴らしい記事がぞくぞくと上げられるので大丈夫でしょう:)

お疲れさまでした!

今回使ったサンプルプロジェクトはhttps://github.com/keroxp/XcodeCIにアップしておいたのでよければ確認して下さい。