travis
ionic
fastlane
OriginalmedibaDay 4

ionic で CI してみた(CIするとは言っていない)

まえがき

この記事は Ionic Advent Calendar / mediba Advent Calendar 2017 の4日目の記事です。

佐藤よしあきと申します
株式会社medibaにて、ionic使ってiOS/Androidアプリを作ってます

今回は、そのアプリ開発で使用している ionic において、CI(継続的インテグレーション)をやってみた結果を、記録しておきます

使うもの

  • GitHub
  • Travis CI
  • Fastlane

これら各々の使い方解説とかは省略します、多分ググれば山ほど出てくるので

ionic/cordovaならではのつらみ

cordovaはマルチプラットフォームなフレームワークです
一つのコードツリーから、複数のプラットフォーム向けのアプリを生成できます

が、いきなりバイナリをぽんとビルドできるわけではありません
大概出来るのですが、 Apple のもの (iOSとMacOS向けのもの) については Apple のビルド環境を使わないとバイナリを作れないため、Cordovaから [Appleのビルド環境向けのコードを生成] し、それをAppleのビルド環境でビルドしないといけません
狭量ですね

仕方がないので、Travis上でもその流れを再現します
ionic/cordova自体はWindowsとかMacOSとか向けにもビルドできるのですが、今回はAndroidとiOSの2つを対象にします

こんな風に2つになる

travis の領域

travisにやってもらう事は、下記になります

  • ビルドするための環境セットアップ
  • fastlane の呼び出し

iOS と Android 両方ビルドする

というわけでAndroidとiOSの両方をビルドすることになるのですが、
* iOSのビルドはMacOS環境でないと 出来ない
* AndroidのビルドはLinux環境でないとめんどくさい
という、また困った問題があります

仕方がないので、一回のTravisジョブで、2つの環境でビルドが走るようにしましょう
これには Travis の仕組みのうち Matrix を利用して対応します

matrix を利用して Android と iOS のビルドを並行で走らせる

下記のように書くことで、 osx と linux のイメージを用いたTravis処理が同時に走ってくれます
os セクションの下にそれぞれの設定を書くことで、 OS イメージ毎の設定を定義していきます

travis.yml
matrix:
  include:
  - os: osx
    osx_image: xcode9.1
    language: objective-c
  - os: linux
    language: android
    android:
      components:
      - tools
      - platform-tools
      - build-tools-26.0.2
      - android-26
      licenses:
      - android-sdk-license-.+
    addons:
      apt:
        packages:
        - oracle-java8-installer

で、before_install セクションなどで下記のように環境毎のセットアップを行います
TRAVIS_OS_NAME にはMatrixにおけるOS指定も反映されるので、それを元に処理を分けられます

travis.yml
before_install:

- if [ ${TRAVIS_OS_NAME} = "linux" ]; then 
    jdk_switcher use oraclejdk8; 
  fi
- if [ ${TRAVIS_OS_NAME} = "osx" ]; then 
      gem install cocoapods; 
      npm install -g ios-sim;
      npm install -g ios-deploy; 
  fi

本来、 ruby とかの複数バージョンごとにビルドを走らせて全部でテストが通ることを確認する、みたいな使い方が想定されていたみたいですが、別にプラットフォーム別ビルドを同時並行で動かしてもいいですよね?

ionic ビルド環境を travis 上にセットアップする

事前にやっておかなきゃいけないことが、わりとあります

  • rvmnvm でそれぞれ ruby と node の任意のバージョンをインストール
  • bundle install で ruby環境の依存モジュールを導入
  • Android : linux の場合、使用するJDKを jdk_switcher で指定
  • iOS : osx の場合、 cocoapods と ios-sim ios-deploy をインストール
  • cordova / ionic / typescript をnpmでグローバルインストール
  • npm install でnodeの依存モジュールを導入

これだけやって、やっと ionic cordova build のための環境が揃います
.travis.ymlに書くと、こんな感じになります

travis.yml
before_install:
- rvm install 2.3.1
- gem install bundler
- bundle install
- nvm install v6
- npm update npm
- if [ ${TRAVIS_OS_NAME} = "linux" ]; then jdk_switcher use oraclejdk8; fi
- if [ ${TRAVIS_OS_NAME} = "osx" ]; then gem install cocoapods; npm install -g ios-sim;
  npm install -g ios-deploy; fi
install:
- npm install -g cordova@7.1.0 ionic@3.18.0 typescript@2.4.2
- npm install

バージョンなどは次節に応じて変更して下さい
ローカルの開発環境では一回やってしまえば(バージョンアップの時とか以外は)気にしないでいい所ですが、CI環境は毎回コレを作り直してもらわないといけないんですね、大変....

fastlane を呼ぶ準備をする

scriptセクションは適当にtestでも通してもらうとして、after_success セクションを用いて fastlane を呼びます
せっかく fastlaneにはビルドの種別などを指定する方法があるので、fastlaneの呼び出し方でビルドの仕方を分岐させることにします

travis.yml
after_success:
- if [ ${TRAVIS_PULL_REQUEST} = "false" ] && [ ${TRAVIS_BRANCH} = "master" ] && [
  ${TRAVIS_OS_NAME} = "linux" ] && [ ${TRAVIS_TAG} ]; then 
    bundle exec fastlane android deploy target:prod; 
  fi
- if [ ${TRAVIS_PULL_REQUEST} = "false" ] && [[ ${TRAVIS_BRANCH} =~ ^[Rr]elease ]] && [ ${TRAVIS_OS_NAME} = "linux" ]; then 
    bundle exec fastlane android deploy target:dev; 
  fi

こんなふうに書くと、 PRではなくて、ブランチ名がそれぞれに合致する、OSがLinux、の場合、 指定のオプションでfastlane を実行、という流れに出来ます
※ ここではAndroidのぶんしか書いてませんが、iOSの場合はTravis実行環境はmacosになるので、単に ${TRAVIS_OS_NAME} = "osx" ってすればいいだけです

fastlane ツールの領域

fastlaneには、 ionic cordova build の実行と、それによって出力された ./platforms/ 以下を用いた実際のビルドを担当してもらいます
ビルドに必要な設定の投入なども、ココで行います

fastlane の基本

fastlaneの起動の仕方は、こんな感じにしています

bundle exec fastlane <<platform>> <<startlane>> [options]
例) bundle exec fastlane ios deploy target:prod

fastfile の中ではこれらのパラメータを下記のように利用します

platform と startlane

bundle exec fastlane ios deploy target:prod の例の場合、 fastfile にこんなふうに書いておけば、ブロックの中の sh() が実行されます

  platform :ios do |options|
    lane : deploy do |options|
      sh("ionic cordova build")
    end
  end

自前でlaneを作ることもできるし、下記のように書くと、外部から呼べない android_deploy という lane を作ったりも出来ます

  private_lane :android_deploy do |options|
  end

コレを用いて、各OS用のビルドの流れを用意します

options : テスト向けか本番向けかでビルド内容を変更する

fastlaneのコマンドラインでは key:val という形で任意の文字列をパラメータとして渡せます
例えば bundle exec fastlane ios deploy target:dev などとすると、 fastfile 内における options[:target] の内容が dev になります

コレを使うと、こんな風にビルドに使用するパラメータを切り替えて定義できます

    if options[:target] == "prod" then
      options[:app_identifier] = "jp.mediba.ysato.test.travistest"
      options[:display_name] = "Travis Test App"
    else
      options[:app_identifier] = "jp.mediba.ysato.test.travistest.dev"
      options[:display_name] = "dev Travis Test App"
    end

今の所は開発版か本番かくらいしか指定していませんが、他にも便利な使い方があるかもしれませんね

iOS : xcodeでやるような環境設定を自動化する

いわゆるinfo.plistの編集とか、Provisioning Profileの指定とか、ionic cordova build iosの後に xcodeを立ち上げて platforms/ios/xxx.xcworkspace を開いてGUIからぽちぽちやるような設定の類、ありますよね
アレを自動で行わないと、Travis上での自動ビルドは完結しません

./platforms/ios/ 以下もリポジトリに入れてしまえばいいんだけど、変更管理が死ぬんですよ....

逆に、これらの設定変更を fastlane で自動化出来ると、例えば platform を初期化しちゃった時とかでも fastlane の当該レーンを通してあげることで、間違いなく全ての設定を反映できるようになります
今となっては、ローカルでも ionic cordova build ios を呼ぶのではなく、 必ずこれを含んだ bundle exec fastlane ios build を使ってビルドするようにしています
※ 画面ローテーションの設定とか、忘れるんだ....

ionic cliでの定義 : 署名やProvisioning Profile関連を自動で定義する

ionic cordova build ios に対して xcodebuilg のオプションを放り込むことで、CI環境上でチームやら署名やらの設定を直接放り込めます
こうすることで、ionic cordova buildが終わった時点で署名関係の設定は既に終了している、という状態を作れるわけです

    options[:buildoptions] = ' -- --developmentTeam="' + options[:team_id] + '" --codeSignIdentity="' + options[:code_sign_identity] + '" --provisioningProfile="' + options[:provisioning_profile] + '" --packageType="' + options[:export_method] + '"'

    sh ("cd .. && ionic cordova build " + options[:platform] + " --prod --release" + options[:buildoptions])

info.plist関連

予めこんな風に、Projectやplistへのパスを定義しておきます

options[:PROJ_PATH] = "platforms/ios/TravisExample"
options[:PLIST_PATH] = "TravisExample/TravisExample-Info.plist"
targetPlistPath = File.join(options[:PROJ_PATH], '..', options[:PLIST_PATH])

あとは下記のように設定を並べていけばいいです
plist設定に必要なkeyなどは都度調べて下さい、いつどう変えられて使えなくなるか分からないので

アプリの Identifier を更新

update_app_identifier(
  xcodeproj: options[:PROJ_PATH],
  plist_path: options[:PLIST_PATH],
  app_identifier: "jp.mediba.ysato.test.travistest.dev"
)

表示名(アイコンの下に表示されるアレ)を変更

update_info_plist(
  xcodeproj: options[:PROJ_PATH],
  plist_path: options[:PLIST_PATH],
  display_name: "[DEV] Travis Test"
)

それ以外の info.plist 系更新

set_info_plist_value(
  path: targetPlistPath,
  key: "NSCameraUsageDescription",
  value: "フレーム撮影の為、カメラにアクセスします"
)            

対象が Arrayの場合、言われてみればそのまんま

set_info_plist_value(
  path: targetPlistPath,
  key: "UISupportedInterfaceOrientations",
  value: ["UIInterfaceOrientationPortrait"]
)

あとは sigh() って gym() れば ipaファイルが出来上がります

Android : 自動で署名したい

ionic cordova build で作成されるapkファイルは未署名の状態なので、このまま配布したりとかPlayストアに登録したりとかは、出来ません
なので署名するのですが、ココをコマンドラインで自動化するには、こんな感じにします

sh("cd .. && jarsigner -verbose  -storepass xxxx -sigalg SHA1withRSA -digestalg SHA1  -keystore target.keystore target/apk/path.apk alias_name")
sh("cd .. && " + ENV['ANDROID_HOME'] + "/build-tools/26.0.2/zipalign -f -v 4 target/apk/path.apk output_apk_filename.apk")

fastlaneでは sh() を使ってコマンドライン実行をかけられるので、こんな風に未署名apk に jarsingerとzipalignをかけてあげればOKです
apkファイル名とかパスとかは適宜読み替えてくださいね

これでapkが出来上がります

あとはお好みの配信プラットフォームで

今は fabric.io を使用してテスト配信を行っているので、Android/iOSどちらも fastlane 内で crashlytics() を使用して配布するようにしました

まとめ

かなり乱暴、というか場当たりに対処を並べた感じになりますが、一応Travisでもビルドは行えます
ただ、実用に足りるかというとちょっと疑問符が付く状態です
環境作って、 ionic/cordovaでビルドして、各プラットフォームでバイナリ作って、と手間が多いので、 トータルで30分かかる んですよね、コレ

なので今現在は、Travis上でのビルドは止めています....
この辺、実用の範囲で収めるために皆さんがどうしているか、聞いてみたいところです....

ただ、この環境を作るための試行錯誤の結果、fastlaneを使用してコマンドライン一発で環境設定を含めたビルド→リリースを行える状態に持っていけたので、その点頑張った甲斐はあったかな、と思っています

ionicでバイナリ作る際の労力を少しでも減らすために、何かの参考になれば、幸いです