Rocket.Chat.Android.LilyをCircle CIで自動ビルドしてFabric betaにアップロード


先日、無事に(?)Rocket.Chat.Androidのコミッタになりました。（プロジェクト名は[Lily](https://github.com/RocketChat/Rocket.Chat.Android.Lily)）
ということで、「自分の環境だけでビルドができればいいや」みたいな考えは捨て、CircleCIでビルドできるようにしました。

Rocket.Chatチームから要望があったのでFabric betaでのアプリ配布もふくめて対応をしたのですが、案外ハマリポイントが多かったので、共有のため記事を残しておきたいと思います。

## 方針

* circle.yml を作らない、なるべくCircleCIのProject Settingsで頑張る
* Android StudioでもCIでも使えるように、環境変数の依存性は少なく
* Fabricプラグインは邪魔くさいので使わない

## CIでビルドを通すために…

### JAVA_HOMEの設定

Rocket.Chat.Android.Lilyでは、retrolambdaをつかっているためか、Java 8じゃないとビルドが通りません。
Circle CIの環境はデフォルトだとJava7が設定されているので、ビルドにコケます。
環境変数のところで

NAME      | VALUE
----------+---------------------
JAVA_HOME |/usr/lib/jvm/jdk1.8.0


と設定します。

### ANDROID_HOMEの設定・・・は不要！

CIがデフォルトで入れてくれるらしい。

### git submodule の設定、local.propertiesのセット

Travis CIだったら勝手に `git submodule init && git submodule update` ってやってくれるんですが、CircleCIではやってくれません。
あと、local.propertiesに`sdk.dir`ってのを定義してないとビルドが通りません。

Pre-dependency commandsのところに

```
git submodule init && git submodule update
echo "sdk.dir="$ANDROID_HOME > local.properties 
```

と設定します。

## ビルドコマンドに注意

Circle CIは、メモリ使用が4GBこえるとプロセス強制終了される、という動作仕様があり、

`./gradlew assembleDebug` だと、

```
> Building 93% > :app:preDexDebug

> Building 93% > :app:preDexDebug./gradlew assembleDebug --info --stacktrace died unexpectedly
```

みたいなかんじで突然死を食らわされることがあります。

どっかのStackOverfowで紹介されてた（わすれたｗ）とおり

```
./gradlew assembleDebug --stacktrace -Pcom.android.build.threadPoolSize=1 -Dorg.gradle.parallel=false -Dorg.gradle.jvmargs="-Xms512m -Xmx2048m" -Dorg.gradle.daemon=false
```

こんなかんじでビルドコマンドをかけば、強制終了することなくアプリのビルドが完了します。



## Fabric SDKの組み込み

リファレンスに載ってはいますが、それなりにやることがあります。

```diff:Fabricの手動組み込み
diff --git a/build.gradle b/build.gradle
index 9e8f838..c1d3551 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,20 +3,26 @@
 buildscript {
     repositories {
         jcenter()
+        maven { url 'https://maven.fabric.io/public' }
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:1.3.0'
+        classpath 'com.android.tools.build:gradle:1.5.0'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
         classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
         classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
+
+        // The Fabric Gradle plugin uses an open ended version to react
+        // quickly to Android tooling updates
+        classpath 'io.fabric.tools:gradle:1.+'
     }
 }
 
 allprojects {
     repositories {
         jcenter()
+        maven { url 'https://maven.fabric.io/public' }
     }
 }

diff --git a/app/build.gradle b/app/build.gradle
index b979a36..cd0f12f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,17 +1,22 @@
 apply plugin: 'com.android.application'
 apply plugin: 'com.neenbedankt.android-apt'
 apply plugin: 'com.jakewharton.hugo'
+apply plugin: 'io.fabric'
 
 android {
     compileSdkVersion 23
     buildToolsVersion "23.0.1"
 
+    Properties properties = new Properties()
+    properties.load(project.rootProject.file('local.properties').newDataInputStream())
+
     defaultConfig {
         applicationId "chat.rocket.android"
         minSdkVersion 16
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
+        manifestPlaceholders = [fabric_api_key:properties.getProperty("FABRIC_API_KEY", System.getenv("FABRIC_API_KEY"))]
     }
     buildTypes {
         release {
@@ -41,4 +46,9 @@ dependencies {
 
     compile 'com.facebook.stetho:stetho:1.2.0'
     compile 'com.facebook.stetho:stetho-okhttp:1.2.0'
+
+    // Crashlytics Kit
+    compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') {
+        transitive = true
+    }
 }

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d48f872..dae2df4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,6 +32,10 @@
             android:authorities="chat.rocket.android"
             android:name=".content.RocketChatProvider"
             android:exported="true"/>
+
+        <meta-data
+            android:name="io.fabric.ApiKey"
+            android:value="${fabric_api_key}" />
     </application>
 
 </manifest>
```

FABRIC_API_KEYのところがちょっと工夫してあって、

* local.properties に `FABRIC_API_KEY=XXXxxxXXXXxxx` の定義があればそれを使う
* ↑がなければ、環境変数の `$FABRIC_API_KEY` を使う

という構成にしてます。前者がAndroid Studioでビルド通るようにするため、後者はCircle CIで楽するために、こういう感じにしました。


## Fabric SDKの初期化処理を追加

アプリケーションクラスに `Fabric.with()` の呼び出しを追加すればよいです。

```diff:RocketChatApplication.java

diff --git a/app/src/main/java/chat/rocket/android/RocketChatApplication.java b/app/src/main/java/chat/rocket/android/RocketChatApplication.java
index d837b0b..62b0961 100644
--- a/app/src/main/java/chat/rocket/android/RocketChatApplication.java
+++ b/app/src/main/java/chat/rocket/android/RocketChatApplication.java
@@ -2,12 +2,15 @@ package chat.rocket.android;
 
 import android.app.Application;
 
+import com.crashlytics.android.Crashlytics;
+import com.crashlytics.android.core.CrashlyticsCore;
 import com.facebook.stetho.Stetho;
 import com.facebook.stetho.okhttp.StethoInterceptor;
 import com.squareup.picasso.OkHttpDownloader;
 import com.squareup.picasso.Picasso;
 
 import chat.rocket.android.api.OkHttpHelper;
+import io.fabric.sdk.android.Fabric;
 
 public class RocketChatApplication extends Application {
     @Override
@@ -25,5 +28,8 @@ public class RocketChatApplication extends Application {
 
         Picasso picasso = new Picasso.Builder(this).downloader(new OkHttpDownloader(OkHttpHelper.getClient())).build();
         Picasso.setSingletonInstance(picasso);
+
+        CrashlyticsCore core = new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build();
+        Fabric.with(this, new Crashlytics.Builder().core(core).build());
     }
 }
```
 
デバッグ中のクラッシュがバンバン報告されるのは勘弁なので、[Crashlyticsをデバッグビルドでは無効化](http://qiita.com/YusukeIwaki/items/e767c45edad48302cec8)でコメント頂いたのを参考に、DEBUGビルドでは無効になるようにしてます。


## Beta用の設定ファイルをつくる

```app/fabric.properties
apiSecret=XXXXXXxxxXXXXXXxxxxxxXXXXXXxxxXXXXXXxxxxxx
apiKey=XXXXXXxxxXXXXXXxxxxxx
```

ローカルでビルドする用には、↑のようなファイルを持っておけばいいだけなんですが、こんな機密情報含んだファイルをGitHubに上げるわけにいきません。
CIでは環境変数にこういう機密情報(?)を設定しておくことができるので、それを利用します

![環境変数](https://qiita-image-store.s3.amazonaws.com/0/74571/641b2174-54ee-4658-8a6d-2b2984fcba4d.png "環境変数")

↑のように`FABRIC_API_KEY`と`FABRIC_BUILD_SECRET`をあらかじめ入れておいて、

![スクリプト](https://qiita-image-store.s3.amazonaws.com/0/74571/8c220d21-cf2f-0a63-b339-674b40ed4e6b.png "スクリプト")

↑のようにそれをechoしてapp/fabric.propertiesに詰め込みます。

## β版配布時のリリースノート

Circle CIは、circle.ymlで制限しないかぎりは馬鹿正直に全ブランチの全コミットに対してビルドをしようとするので

* どのブランチをビルドしたものか
* どのコミットを取り込んだものか

くらいはリリースノートで見れないと、わりと困ったことになります。

```diff:app/build.gradle
diff --git a/app/build.gradle b/app/build.gradle
index cd0f12f..bad25e6 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,6 +3,12 @@ apply plugin: 'com.neenbedankt.android-apt'
 apply plugin: 'com.jakewharton.hugo'
 apply plugin: 'io.fabric'
 
+def getCurrentCommitMessage() {
+    def gitlog = 'git log origin/develop...HEAD --decorate'.execute().text
+    if (gitlog.trim().length()>0) return gitlog
+    return 'git log -n 1 --decorate'.execute().text
+}
+
 android {
     compileSdkVersion 23
     buildToolsVersion "23.0.1"
@@ -17,6 +23,8 @@ android {
         versionCode 1
         versionName "1.0"
         manifestPlaceholders = [fabric_api_key:properties.getProperty("FABRIC_API_KEY", System.getenv("FABRIC_API_KEY"))]
+        ext.betaDistributionNotifications=false
+        ext.betaDistributionReleaseNotes=getCurrentCommitMessage()
     }
     buildTypes {
         release {
```

とりあえず、超適当ですが、 git log --decorate で 「origin/develop〜HEADまでのコミット または 最新の1コミット」をだすようにしてみました。

## Beta配布！

![./gradlew crashlyticsUploadDistributionDebug](https://qiita-image-store.s3.amazonaws.com/0/74571/f4b25e8b-d759-2744-6184-3866207902fe.png "./gradlew crashlyticsUploadDistributionDebug")

こんな感じの Post-test commands を定義して、CIでビルドをかけると・・・

![ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!](https://qiita-image-store.s3.amazonaws.com/0/74571/82cf59ec-c36e-80f3-0b57-e381999ae45f.png "ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!")

ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!　ってなります。