24/7 twenty-four seven

iOS/OS X application programing topics.

ユビレジのiPadアプリのCI環境をJenkinsからTravis CIに移行したときのまとめ


こちらの記事について、最新のTravis CIの環境(2014/4/15)ではコード署名に失敗する問題があります。
その問題の修正については下記の記事にまとめました。
Travis CIでipaを作るときのCode Signが失敗するのを修正したメモ - 24/7 twenty-four seven


実際は完全に移行したわけではなくて、Travis CIの有料プラン(プライベートリポジトリが使える)のフリートライアルを試しているところなのですが、しばらくはTravis CIでCIを動かすことにしたので、そのときの設定などをまとめます。


もともとは社内のサーバでJenkinsをホストしていて、それがダメということは全然ないのですが、社内でサーバをメンテナンスするのも面倒だし、ビルドスクリプトとかをポータブルな状態にしておくのは手元でサクッと実行できたりいろいろ都合が良さそうだと思い、試しにやってみることにしました。


Travis CIを選んだのは個人のOSSで使っていて慣れてるからと、Xcodeのビルド環境をホストしてくれるCIサービスの選択肢はそれほどないからです。
あと昨日知ったのですがCloudBeesiOS/MacのCIをサポートしてるということなので、こちらも試してみようかなと考えています。


やりたいことは

  • Githubにプッシュしたらそのタイミングでビルド&テストを実行
  • テストが成功したらTestFlightとCrittercismにアップロード
  • Pull Requestに対してもテストをしたい

の3点です。


副産物としてApple Developer Centerから最新のProvisioning Profileを自動的にダウンロードして使用する仕組みを作ったので、デバイスを追加したときに合わせてCI環境のProvisioning Profileをメンテナンスしなくてよくなりました。
ただ、この問題はエンタープライズのDeveloper Program(¥24,800/年)を購入するほうがスマートに解決できます(iOS Developer Enterprise ProgramのProvisioning Profileはデバイスの制限がなくなるので)。

ビルドスクリプトなど

ビルドスクリプトはrakeで書きました。
↓ Rakefileの全体はこちら

↓ .travis.ymlはこちら


Rakefileの設計は下記を参考にしました。


TestFlightへのアップロードは下記を参考にしました。


Crittercismへのアップロードは下記を参考にしました。


Apple Developer CenterからProvisioning Profileをダウンロードするスクリプトは下記を利用しました。


長いので要点だけ解説します。

ユニットテストの実行

テストの実行は54行目からの下記の部分です。

task :test do |t|
  DESTINATIONS.each do |destination|
    options = {
      sdk: 'iphonesimulator',
      workspace: WORKSPACE_DIR,
      scheme: SCHEME,
      configuration: 'Debug',
      destination: destination
    }
    options = join_option(options: options, prefix: "-", seperator: " ")
    sh "xcodebuild #{options} test | xcpretty -tc" do |ok, res|
      fail unless ok
    end
  end
end


コマンドラインで

$ rake test

とすると、下記のようなコマンドが実行されます。
DESTINATIONSにはとりあえずすべてのOSバージョンが書いてあるので、6.0, 6.1, 7.0, 7.0 (64 bit)のそれぞれのiPadシミュレータでテストが実行されます。

xcodebuild -sdk "iphonesimulator" -workspace "/Users/travis/build/ubiregiinc/ubiregi-client/Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Debug" -destination "name=iPad,OS=6.0" test | xcpretty -tc

アプリケーションのビルド

テストが成功したらTestFlightにAdHoc版としてアップロードするので、アプリケーションをビルドしてipaファイルを作ります。
ビルドは70行目〜、ipaファイルを作る部分にあたるのは147行目からです。

テストの場合とほぼ同じですが、後の工程でビルドされたappパッケージからipaファイルを作ったり、dSYMファイルをzipにしたりするので、OBJROOTとSYMROOTを指定してビルド先のディレクトリを扱いやすくしています。

また、ここでコード署名をしておかないと、ipaを作るときにAdHoc用のProvisioning Profileを埋め込むことができなかったので、署名だけしています。
Provisioning Profileまで指定すると今度はTestFlightにアップロードするときに失敗したのでProvisioning Profileの指定は空にしています。このへんは何度か設定を変えながらやってみたらできたという感じで、よくわかりません。

desc "Build application"
task :build do |t|
  options = {
    sdk: SDK,
    workspace: WORKSPACE_DIR,
    scheme: SCHEME,
    configuration: CONFIGURATION
  }
  options = join_option(options: options, prefix: "-", seperator: " ")
  settings = {
    OBJROOT: BUILD_DIR,
    SYMROOT: BUILD_DIR,
    CODE_SIGN_IDENTITY: DEVELOPER_NAME,
    PROVISIONING_PROFILE: ""
  }
  settings = join_option(options: settings, prefix: "", seperator: "=")
  sh "xcodebuild #{options} #{settings} clean build | xcpretty -c"
end

desc "Create .ipa file"
task :archive do |t|
  archive
end


トライ&エラーの内容を整理すると、

  • CODE_SIGN_IDENTITYを指定してPROVISIONING_PROFILEを空にすると下記のエラーでビルドが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (Y7522692LT)" PROVISIONING_PROFILE="" clean build
Code Sign error: No matching codesigning identity found: No codesigning identities (i.e. certificate and private key pairs) matching “iPhone Distribution: Ubiregi Inc. (Y7522692LT)” were found.

CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.0'
  • CODE_SIGN_IDENTITYとPROVISIONING_PROFILEを両方とも空にすると下記のエラーでビルドが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" PROVISIONING_PROFILE="" clean build
CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.0'
  • `CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`と指定して(ビルド時にコード署名しない)、PackageApplicationコマンドにsignオプションとembedオプションを指定すると、ビルドは成功するが下記のエラーでipaファイルの作成が失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build  
xcrun -sdk iphoneos PackageApplication -v "./build/Release-iphoneos/Ubiregi2.app" -o "./build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -sign "iPhone Distribution: Ubiregi Inc. (Y7522692LT)" -embed "$HOME/Library/MobileDevice/Provisioning Profiles/Ubiregi AdHoc.mobileprovision"
error: Failed to read entitlements from '/var/folders/9x/jfhnrxj531v5427xj48tsdd00000gn/T/S6ueKkDErz/Payload/Ubiregi2.app'
  • `CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`と指定して(ビルド時にコード署名しない)、PackageApplicationコマンドにembedオプションだけを指定すると、ipaファイルの作成は成功するが下記のエラーでTestFlightへのアップロードが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build  
xcrun -sdk iphoneos PackageApplication -v "./build/Release-iphoneos/Ubiregi2.app" -o "./build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -embed "$HOME/Library/MobileDevice/Provisioning Profiles/Ubiregi AdHoc.mobileprovision"
'Invalid IPA: missing embedded provisioning profile. Are you sure this is an ad hoc?'


上記の結果から考えて、ビルド時にコード署名をして、アーカイブ時にProvisioning Profileを埋め込むという手順でうまくいきました。

ipaファイルの作成

ビルドされたappパッケージからipaファイルを作ります。
「アプリケーションのビルド」のところで述べたように、ここではTestFlightにアップロードするためのAdHoc用のProvisioning Profileを指定します。

def archive
  options = {
    o: IPA_FILE,
    embed: PROVISIONING_PROFILE
  }
  options = join_option(options: options, prefix: "-", seperator: " ")
  sh "xcrun -sdk #{SDK} PackageApplication #{APP_FILE} #{options}"
end


Provisioning Profileを埋め込むためには証明書と秘密鍵(cerファイルやp12ファイル)が必要なので、それらはキーチェーンからエクスポートして、あらかじめリポジトリに含めておきます。

apple.cerはキーチェーンに登録されている「Apple Worldwide Developer Relations Certification Authority」、dist.cerとdist.p12は今回はUbiregi Inc.の「iPhone Distribution」の証明書と秘密鍵です。


そして、上記のコマンドの前に、一時的なキーチェーンを作成してこれらの各種証明書を追加します。
また、Travis CIで実行する分には後始末は必要ないのですが、手元で実行するときには同じ名前のキーチェーンを作ろうとすると失敗するので、コマンドの実行後には先ほど作ったキーチェーンを削除します。

↓ こちらを参考にしました。
Deploy an iOS app to testflight using Travis CI

def add_certificates
  sh "security create-keychain -p travis #{KEYCHAIN_NAME}"
  sh "security import ./certificates/apple.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.p12 -k ~/Library/Keychains/#{KEYCHAIN_NAME} -P #{KEY_PASSWORD} -T /usr/bin/codesign"
  sh "mkdir -p \"#{PROFILE_DIR}\""
  sh "cp \"./profiles/#{PROFILE_NAME}.mobileprovision\" \"#{PROFILE_DIR}\""
end

def remove_certificates
  sh "security delete-keychain #{KEYCHAIN_NAME}"
end

TestFlightとCrittercismにアップロード

ipaファイルをTestFlightに、dSYMファイルをCrittercismにアップロードします。
112行目からのタスクです。

task :crittercism => [DSYM_ZIP_FILE] do |t|
  fields = {
    dsym: "@#{DSYM_ZIP_FILE}",
    key: API_KEY,
  }
  fields = join_option(options: fields, prefix: "-F ", seperator: "=")
  sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" https://api.crittercism.com/api_beta/dsym/#{APP_ID} #{fields} -o /dev/null"
end

desc "Upload IPA file and dSYM file to TestFlight and notify testers"
task :testflight => ["version:set_build_version", IPA_FILE, :crittercism] do |t|
  if ENV['TRAVIS_PULL_REQUEST'] != "false"
    puts "This is a pull request. No deployment will be done."
    next
  end
  if ENV['TRAVIS_BRANCH'] != "master"
    puts "Testing on a branch other than master. No deployment will be done."
    next
  end

  release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S")
  release_notes = "Build: #{InfoPlist.marketing_and_build_version}\nUploaded: #{release_date}"

  fields = {
    file: "@#{IPA_FILE}",
    api_token: API_TOKEN,
    team_token: TEAM_TOKEN,
    notes: release_notes,
    notify: true,
    distribution_lists: DISTRIBUTION_LISTS
  }
  fields = join_option(options: fields, prefix: "-F ", seperator: "=")
  sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http://testflightapp.com/api/builds.json #{fields} -o /dev/null"
end


と、こんな感じでGithubにプッシュすると自動的にテストが実行されて、(masterブランチへのPushなら)TestFlightやCrittercismへのアップロードもやってくれるようになりました。

ビルドスクリプトはそれぞれrakeのタスクとして作ってあるので、手元でも簡単に

rake test

とテストを実行したり、

rake testflight

と簡単にTestFlightにアップロードすることができるようになりました。

注意点

Travis CIのデフォルトのOS Xの環境ではなぜか`pod install`がSegmentation faultで失敗しました。

$ pod install

Analyzing dependencies

/Users/travis/.rvm/gems/ruby-2.0.0-p247/gems/xcodeproj-0.14.1/ext/xcodeproj/xcodeproj_ext.bundle: [BUG] Segmentation fault

ruby 1.8.7 (2012-02-08 patchlevel 358) [universal-darwin12.0]


同様のエラー報告がIssueに登録されていて対処法はとりあえず`rvm: 1.9.3`としてrubyのバージョンを1.9.3に固定するとのことでした。
iOS - Pod Install Erroring Out · Issue #1657 · travis-ci/travis-ci · GitHub


1.9.3だとキーワード引数が使えないとかrakeを書くときにちょっと制限ができちゃいますが、とりあえずはそうしました。早く修正されるといいですね。

おまけ1:Provisioning Profileの自動ダウンロード

Jenkinsを使っているときからもあった問題に、Provisioning Profileのメンテナンスがあります。
デバイスを追加があったときに、CIのマシンが参照しているProvisioning Profile(リポジトリに含めるなどする)も更新する必要があるからです。

今回はそれも何とかしたいと思って、CIの実行時に毎回最新のものをダウンロードするようにしました。
177行目からのタスクになります。

namespace :profile do
  desc "Download provisioning profiles from Apple Developer Center"
  task :download do
    ruby "./scripts/apple_dev_center.rb -C ./scripts/apple_dev_center.config -d profiles -O /dev/null"
  end
end


下記のスクリプトを実行すると、AppleのWebサイトに接続して、Provisioning Profileをダウンロードしてprofilesディレクトリに保存します。
ipaファイルを作るときにはこのProvisioning Profileが使用されます。

ruby "./scripts/apple_dev_center.rb -C ./scripts/apple_dev_center.config -d profiles -O /dev/null"


Apple Developer CenterからProvisioning Profileをダウンロードするスクリプトは下記を利用しました。
lacostej/apple-dev · GitHub


上記のスクリプトはgemの体裁は整っているのですが、RubyGemsとしては登録されておらず、READMEに載っている使い方もbinのスクリプトを直接利用するように書いてあったので、gemとしてインストールしても使い方がよくわからなかったので、スクリプトごとリポジトリに含めて利用するようにしました。

おまけ2:バージョン番号とビルド番号の設定

TestFlightにアップロードするときにどのビルドかを区別するためにgitのハッシュがビルド番号に設定されるようにします。
257行目です。

desc "Sets build version to last git commit (happens on each build)"
task :set_build_version do
  rev = `git rev-parse --short HEAD`.strip
  puts "Setting build version to: #{rev}"
  InfoPlist.build_version = rev
end

ユビレジではバージョン番号のポリシーとして、「X.XX」という形式で、マイナー番号が偶数がリリース版、奇数ならベータ版という決まりにしているので、申請したらとりあえずマイナーを1つ上げるなどの操作をよくするので、ついでに手元でバージョン番号を簡単に操作できるようにしました。

例えば現在のバージョンが2.56とすると、

rake version:bump:minor

とすると、2.57になります。

rake version:bump:release

とすると、現在のバージョンが2.56でも2.57でも2.58になります。

このとき、ビルド番号はgitのハッシュではなくて、番号っぽくコミット数になるようにしています。
アプリ内にバージョンを表示しているのでこちらのほうが見栄えがなんとなくいいかなと思ってそうしています。

下記を参考にしました。
How to Automatically Update Xcode Build Numbers from Git | Objective C#