Android
CircleCI

Androidアプリ開発でのCircle CI運用例 / Androidアプリを開発する際の俺的設計

More than 1 year has passed since last update.

@eaglesakura です。

私はCIのサービスとしてCircle CIを使用しています。

Jenikinsとくらべて良い点も悪い点も多くの記事で語られていますが、ここでは実際にどういう形でプロジェクトを運用しているか解説します。


Androidアプリ開発のテンプレート設定

私の場合は次のような設定をほぼテンプレートとして使用しています。

この状態からプロジェクトの要件に合わせて変更をかけます。

checkout:

post:
- git submodule update --init
- chmod 755 ./gradlew
- chmod 755 ./script/developer-install-private.sh
# ここにビルド用スクリプトの実行権限を追記していく...
machine:
timezone:
Asia/Tokyo
java:
version: oraclejdk8
environment:
ANDROID_HOME: /home/ubuntu/android-sdk
GRADLE_OPTS: -Dorg.gradle.parallel=false -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xms256m -Xmx3000m -XX:MaxPermSize=128m -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS: -Dfile.encoding=UTF-8 -Xms256m -Xmx3000m -XX:MaxPermSize=128m
dependencies:
cache_directories:
- "/home/ubuntu/android-sdk"
override:
- sh -c "$(curl -fsSL https://raw.githubusercontent.com/eaglesakura/build-dependencies/master/circleci/install-android-sdk-auto.sh)"
- sh -c "$(curl -fsSL https://raw.githubusercontent.com/eaglesakura/build-dependencies/master/circleci/install-android-extra-repo.sh)"
- ./script/developer-install-private.sh
- ./gradlew --refresh-dependencies :app:dependencies
test:
override:
- ./script/circleci-build-testing.sh
- ./script/circleci-build-assemble.sh
deployment:
feature:
branch: /^feature\/id\/.*$/
commands:
- ./script/circleci-deploy-deploygate.sh
- cp -r ./ci-release $CIRCLE_ARTIFACTS
develop:
branch: develop
commands:
- ./script/circleci-deploy-deploygate.sh
- cp -r ./ci-release $CIRCLE_ARTIFACTS
release:
branch: /^v[0-9]\..*$/
commands:
- ./script/circleci-deploy-deploygate.sh
- cp -r ./ci-release $CIRCLE_ARTIFACTS
nightly:
branch: master
commands:
- cp -r ./ci-release $CIRCLE_ARTIFACTS


リポジトリ状態を同期する

githubからの同期はCIが自動的に行なってくれますが、submoduleは自動的に同期されません。

そのため、checkout/postステップで必要なファイルの同期や実行権限付与を行います。

checkout:

post:
- git submodule update --init
- chmod 755 ./gradlew
- chmod 755 ./script/developer-install-private.sh
# ここにビルド用スクリプトの実行権限を追記していく...


Android特有の環境設定

Circle CIではAndroidビルド用に標準でAndroid SDKがインストールされています。

ですが、標準インストール版のAndroid SDKはバージョンが古かったりするので、私の場合は別途取得した上でキャッシュに追加しています。

machine:

environment:
ANDROID_HOME: /home/ubuntu/android-sdk
dependencies:
cache_directories:
- "/home/ubuntu/android-sdk"
- sh -c "$(curl -fsSL https://raw.githubusercontent.com/eaglesakura/build-dependencies/master/circleci/install-android-sdk-auto.sh)"
- sh -c "$(curl -fsSL https://raw.githubusercontent.com/eaglesakura/build-dependencies/master/circleci/install-android-extra-repo.sh)"

インストール用のスクリプトはここに置いてあるので、必要であれば好きに使うなりコピペするなりしてください。


開発者(及びCI)ごとのビルド設定を変更する

プロジェクトに関わる開発者すべてが、同じ状態でビルドするとは限りません。

例えば、一部のソースコードをmavenから取得してビルドする人と、全てソースコードを取得してビルドする人がいる可能性があります。

また、特殊な要件として「本番署名の鍵はCI以外持たない」等の要件も発生するかもしれません。

そういった場合に備えて、私の場合はプロジェクト設定を次のようにしています。

mkdir ./private/

cp -rf ./private.tmp/** ./private/

# .gitignore

/private/

// /private.tmp/private.gradle

/**
* デバッグ用署名設定
*
* 実開発での署名とは異なる。
*/

ext.DEBUG_KEYSTORE_FILE_PATH = file('app/private/sign/appdebug.jks')
ext.DEBUG_KEYSTORE_PASS = "appdebug"
ext.DEBUG_KEYSTORE_ALIAS_NAME = "appdebug"
ext.DEBUG_KEYSTORE_ALIAS_PASS = "appdebug"

/**
* リリース用署名設定
*
* 実際にリリースされるapkは別管理された鍵で署名されるため、
* ソースコードをビルドしただけでは完全に一致するAPKを生成することはできない。
*
* Circle CIのビルドでは正しいリリース用鍵で署名される。
*/

ext.RELEASE_KEYSTORE_FILE_PATH = file('app/private/sign/apprelease.jks')
ext.RELEASE_KEYSTORE_PASS = "apprelease"
ext.RELEASE_KEYSTORE_ALIAS_NAME = "apprelease"
ext.RELEASE_KEYSTORE_ALIAS_PASS = "apprelease"

/**
* DeployGateアップロード用APIキー
*/

ext.DEPLOYGATE_USER_NAME="deploygate_user_name"
ext.DEPLOYGATE_API_KEY="deploygate_api_key"

// /build.gralde

apply from: "private/private.gradle"

多少複雑ですが、プロジェクトでは下記のように処理が行われます。


  1. リポジトリに/private.tempというディレクトリを用意して、private.gradleという設定ファイルや必要なダミーファイルを置く


  2. .gitignore/private/を追記する

  3. 開発者(もしくはCI)はprivate.tmpディレクトリをprivateディレクトリにコピーする


  4. build.gradle から/private/private.gradleをapplyして適用する


  5. ./script/developer-install-private.shは上記をスクリプト化しておくことでセットアップを容易にする

こうすることで、/privateに配置されたファイルは自由に開発者ごとに変更をかけられます。

それぞれのPCで環境変数を設定してもらったり、ビルドオプションを変更するよりも「必要であればprivate.gradleのここを弄ってくれ。弄ってもリポジトリには影響がないから」という説明のほうが楽です。

また、デフォルト値と環境変数をうまくセットアップしておくことで署名ファイルを入れ替えたりすることもできます。


デプロイ

ビルドが完了したら、CIでビルドされたものが正しく動作することを確認しなければなりません。

手元で正しく動いてもCIでビルドしたらうまくいかない、もしくはその逆もありえます。

CIを導入・運用している以上、CI上の成果物が正義ですので、CIから成果物を回収・テストのステップが必要です。

弊社の場合は基本的に Deploygate を使用しています。

これは公式でGradle Pluginが配布されていて容易に設定可能だからです。

また、本番環境(Google Playなど)へのデプロイも可能であればここで行えるようにすると良いでしょう。

私の場合は 運用規則 に則って次のように分岐します。


  • feature


    • ブランチ名 feature/id/${issue番号}/${機能名}

    • issueごとに分岐したブランチ



  • develop


    • ブランチ名 develop

    • 基本的には最新のコード



  • release


    • ブランチ名 v${メジャーバージョン名}.${マイナーバージョン名}

    • リリースごとに作成される

    • このブランチの成果物のみがリリースの対象となる



  • nightly


    • ブランチ名: master

    • 毎夜4時-5時のあいだにビルドが実行される

    • 依存ライブラリの更新等によってビルドが腐っていないかを確認する



deployment:

feature:
branch: /^feature\/id\/.*$/
commands:
- ./script/circleci-deploy-deploygate.sh
- cp -r ./ci-release $CIRCLE_ARTIFACTS
develop:
branch: develop
commands:
- ./script/circleci-deploy-deploygate.sh
- cp -r ./ci-release $CIRCLE_ARTIFACTS
release:
branch: /^v[0-9]\..*$/
commands:
- ./script/circleci-deploy-deploygate.sh
- cp -r ./ci-release $CIRCLE_ARTIFACTS
nightly:
branch: master
commands:
- cp -r ./ci-release $CIRCLE_ARTIFACTS


Circle CIでの自動テスト

2016年Q3現在、エミュレータを用いたUnitTestは行わず、JavaVM上でのテストのみを行っています。

※ディレクトリ的にいうと、 androidTest/ は行わずに test/ のみ行っています。

ただし、PC用の設定でそのままビルドを流すとひどい目にあう(特にメモリ不足)ので、CI専用の設定を置いておくと良いです。

私の場合は次のように設定しています。


build.gradle

// eglibrary.ci.ciRunning はCIビルド中なら"true"となる自作プラグイン

// eglibrary.ci.buildVersionCode はCIビルド中ならCIのビルド番号、そうでないなら"1"となる自作プラグイン
android {

// バージョンコードをビルド番号と一致させる
// プログラム上からVersionCodeを確認することで、Circle CIのビルド番号を知ることができる
// これは後々の検証に役立つ
defaultConfig {
versionCode eglibrary.ci.buildVersionCode as int
}

// メモリ使用量・並列化数を変更する
dexOptions {
javaMaxHeapSize eglibrary.ci.ciRunning ? "1g" : "4g"
maxProcessCount eglibrary.ci.ciRunning ? 1 : 4
threadCount eglibrary.ci.ciRunning ? 1 : 8
preDexLibraries !eglibrary.ci.ciRunning
}

// UnitTestのVM引数を変更する
testOptions {
unitTests.all {
if (eglibrary.ci.ciRunning) {
jvmArgs '-Xmx2048m', '-XX:+HeapDumpOnOutOfMemoryError'
}
}
}
}



テスト実行とレポート回収

テスト実行はbash等のスクリプトに記述しておくほうが良いです。

直接 circle.yml に記述することもできますが、成果物回収等が面倒になります。

また、成果物の回収はテストの成功・失敗によらず必ず行うようにしましょう。

でないと、もしテストが失敗しても原因がわからずに無駄な時間を過ごしてしまいます。

また、レポートはHTMLだけでなくXMLも回収するようにしてください。

Circle CIはXML形式のレポート格納場所 ${CIRCLE_TEST_REPORTS} を用意してくれています。

ここに成果物を回収することで、「テスト実行が遅い」「どのテストが失敗した」を表示してくれるようになります。

#! /bin/sh

report_cp() {
mkdir "$CIRCLE_ARTIFACTS/app"
mkdir "$CIRCLE_TEST_REPORTS/junit/"
mkdir "$CIRCLE_TEST_REPORTS/junit/googleplayDebug"

cp -r ./app/build/reports "$CIRCLE_ARTIFACTS/app"
find . -type f -regex ".*/build/test-results/googleplayDebug/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/googleplayDebug/ \;
}

# テスト実行
./gradlew -PpreDexEnable=false -Pcom.android.build.threadPoolSize=1 \
:app:testGoogleplayDebugUnitTest

if [ $? -ne 0 ]; then
echo "UnitTest failed..."
report_cp
exit 1
else
report_cp
fi


UnitTestを分割して実行する

UnitTestが巨大になると、UnitTestがタイム・アウトしたりメモリ不足で落ちたりする場合があります。

私の場合、データベース系のUnitTestを大量に記述したところ、メモリ不足に陥りました。

Gradleでは「特定パッケージのテストのみを実行する」という手段が用意されています。

./gradlew :app:testDebugUnitTest --tests "com.eaglesakura.test.package_name.**"

ですが、「特定パッケージ以外のテストを実行する」というオプションが見当たりませんでした。

仕方ないので、今のところ次のようにbuild.gradleを加工して変則的にビルドしています。

UnitTestは独立性が高いので、こういう変則的なことをしても問題なく実行できます。

# app.test.smallTests=trueの環境変数を与えてビルドする

./gradlew -Dapp.test.smallTests=true :app:testDebugUnitTest

android {

sourceSets {
// UnitTestで、"app.test.smallTests"の環境変数がtrueならば
// 一部のテストを重いディレクトリをソースコードから外す
test {
if ("true" == "${System.properties["app.test.smallTests"]}") {
java.exclude "path/to/exclude/package/**"
}
}
}
}


retrolambdaを使用する場合のUnitTest追記

Retrolambda+RobolectricでUnitTestするときのメモ を参照し、Java8としてビルドされるようにしてください。


やっておくといざという時に役立つTips


dependenciesの書き出しをしておく

gradleでは簡単に依存ライブラリを追加することができます。

バージョンを完全に固定するとライブラリのアップデートによるbug fix等に追従できない等の問題が発生します。

逆に、「ひさびさにビルドしたらライブラリがアップデートされて動かない!納品時点でのバージョンはどれだよ!!?」となるパターンもあります。

そういった悲劇を防ぐため、次の1行を入れておくだけで「ああ、このビルド時点ではこのバージョンが使われてたんだな」と最悪の場合でもコードを追いかけることができるようになります。

dependencies:

override:
# 適当なところにコレを追加
- ./gradlew --refresh-dependencies :app:dependencies

dependencies タスクは依存(依存ライブラリが依存しているライブラリ等、すべての依存関係)の詳細なバージョン一覧を出力することができます。

--refresh-dependencies をつけることにより、常にmaven repositoryの最新を追いかけることができるため、mavenリポジトリが頻繁に更新されても問題ありません。


便利なプラグイン

入れておくと役立つプラグイン一覧


CIは誰のためか

最終的には自分が楽をするためにあります。

正しくセットアップし、継続的に運用できるようにしましょう。