Android App Bundleをコマンドラインで署名して生成する方法

## はじめに

初めてAndroidアプリのリリースビルドを行ったのですが、いくつかつまづいたところがあったので、体系的にまとめることにします。

Android Studioで行うとかんたんだと公式ドキュメントに記載されていますが、今後CD環境を構築することを考慮し、コマンドラインでビルドすることにしました。

## 「Android App Bundle」とは？

拡張子が（ `.aab` ）であることもあり、本記事では「AAB」と略します。

アプリのコードとリソースをモジュールにまとめる署名付きのバイナリファイルです。
AABをPlay Consoleにアップロードすることで、ユーザーに提供するさまざまなAPKを自動で生成します。

Android Studio 3.2 以降で使えます。

## AABのメリット

従来のAPK形式と比べ、以下のメリットがあります。

メリットは他にもあります。詳細は公式ドキュメントをご参照ください。
https://developer.android.com/platform/technology/app-bundle?hl=ja

### APKを効率的に管理できる

1つのAABからさまざまなAPKを生成できるため、APKを1つずつ署名やアップロードする必要がなくなります。

### アプリのサイズが小さくなる

Google PlayのDynamic DeliveryはAABを使い、各デバイスの設定に合わせて最適化したAPKをビルドして配信します。
ユーザーは自分のデバイスでのみ使われるコードとリソースのみダウンロードされるようになり、アプリのサイズが小さくなります。

### ビルド時間が短縮される

Gradleなどによるビルドは、モジュール式アプリ向けに最適化されているため、大規模なモノリシックアプリに比べてビルド時間を大幅に短縮できます。

## 環境

- OS：macOS Mojave 10.14.6
- Android Studio: 3.6.1
- Kotlin：1.3.72
- Gradle：6.3
- Gradle plugin：3.6.3

## AABの生成手順

AABの生成手順を説明します。

### 秘密鍵の生成

まず、署名に使う「Keystore（キーストア）」と呼ばれる秘密鍵を生成します。

公式ドキュメントに記載されている通りのコマンドを実行します。
https://developer.android.com/studio/build/building-cmdline?hl=ja

```shell-session
$ keytool -genkey -v -keystore {Keystore name} -keyalg RSA -keysize 2048 -validity 10000 -alias {Alias name}
```

私はDroidKaigi 2020のアプリを参考に、キーストア名を `release.keystore` 、エイリアス名を対象アプリ名（私の場合は `uhooipicbook` ）にしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L34
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L46

```shell-session
$ keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias uhooipicbook
```

秘密鍵はウィザード形式で生成します。

```shell-session
キーストアのパスワードを入力してください:  
```

キーストアのパスワードを入力します。
後ほど使うので、忘れないようにしてください。

```shell-session
新規パスワードを再入力してください: 
```

確認のため、キーストアのパスワードを再度入力します。

```shell-session
姓名は何ですか。
```

自分の名前をフルネームで入力します。
私は `uhooi` と入力しました。

```shell-session
組織単位名は何ですか。
```

所属組織の単位名を入力します。
私は個人開発なので `personal` と入力しました。

```shell-session
組織名は何ですか。
```

組織の名前を入力します。
私は `THE Uhooi` と入力しました。

```shell-session
都市名または地域名は何ですか。
```

都市名または地域名を入力します。
私は `Setagaya-ku` と入力しました。

```shell-session
都道府県名または州名は何ですか。
```

日本の場合、都道府県名を入力します。
私は `Tokyo` と入力しました。

```shell-session
この単位に該当する2文字の国コードは何ですか。
```

国コードを入力します。
私は `JP` と入力しました。

```shell-session
CN=uhooi, OU=personal, O=THE Uhooi, L=Setagaya-ku, ST=Tokyo, C=JPでよろしいですか。
```

確認のため、今まで入力した内容が表示されます。
修正する場合はEnterキーを押下し、問題ない場合は `y` を入力します。

```shell-session
10,000日間有効な2,048ビットのRSAの鍵ペアと自己署名型証明書(SHA256withRSA)を生成しています
        ディレクトリ名: CN=uhooi, OU=personal, O=THE Uhooi, L=Setagaya-ku, ST=Tokyo, C=JP
<uhooipicbook>の鍵パスワードを入力してください
        (キーストアのパスワードと同じ場合はRETURNを押してください): 
```

鍵のパスワードを入力します。
こちらも後ほど使うので、忘れないようにしてください。

```shell-session
新規パスワードを再入力してください: 
```

確認のため、鍵のパスワードを再度入力します。

```shell-session
[release.keystoreを格納中]

Warning:
JKSキーストアは独自の形式を使用しています。"keytool -importkeystore -srckeystore release.keystore -destkeystore release.keystore -deststoretype pkcs12"を使用する業界標準の形式であるPKCS12に移行することをお薦めします。
```

こちらの警告が気になりますが、私は無視してしまいました。対応方法をご存じの方は教えていただけると嬉しいです。

これで現在のフォルダに `release.keystore` が存在したら秘密鍵の生成は完了です。

### 秘密鍵の格納

生成した秘密鍵を対象モジュールのルートフォルダに格納します。
私の場合、 `app` フォルダ直下に格納しました。
<img width="627" alt="スクリーンショット_2020-05-28_15_06_37.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/04970fac-6e8a-3b4a-9ee9-83f1741510de.jpeg">

### 秘密鍵をバージョン管理から無視

秘密鍵は外部に漏れてはいけないため、パブリックリポジトリで開発している場合はバージョン管理の対象外にします。

Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。

```diff:.gitignore
+ *.keystore
```

デバッグ用の秘密鍵をコミットしたい場合は、ワイルドカードで指定した上で例外として定義すると安全です。

```:.gitignore
*.keystore
!app/debug.keystore
```

DroidKaigi 2020のアプリを参考にしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/.gitignore#L250-L251

### `build.gradle` に署名の設定を追加

モジュールレベルの `build.gradle` に署名の設定を追加します。

プライベートリポジトリの場合、キーストアと鍵のパスワードを直接記述してもいいと思います。
パブリックリポジトリの場合はパスワードが流出したら問題なので、何らかの方法で隠蔽する必要があります。
私はDroidKaigi 2020のアプリを参考に、環境変数から取得するようにしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L45
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L47

```diff_groovy:/app/build.gradle
android {
+     signingConfigs {
+         release {
+             storeFile file("release.keystore")
+             storePassword System.getenv("RELEASE_KEYSTORE_STORE_PASSWORD")
+             keyAlias "uhooipicbook"
+             keyPassword System.getenv("RELEASE_KEYSTORE_KEY_PASSWORD")
+         }
+     }
    buildTypes {
        release {
+             signingConfig signingConfigs.release
        }
    }
}
```

パスワードの隠蔽方法は環境変数を使う以外にもあり、公式ドキュメントでは `keystore.properties` を使う方法が紹介されています。
https://developer.android.com/studio/publish/app-signing?hl=ja#secure-shared-keystore

### AABの生成

ここまでで設定は完了です。

あとはキーストアと鍵のパスワードをエクスポートし、対象モジュールで `bundle{Variant}` タスクを実行することで、AABが生成されます。
私の場合、 `app` モジュールの `Release` バリアントなので、以下のコマンドを実行します。

```shell-session
$ export RELEASE_KEYSTORE_STORE_PASSWORD={キーストアのパスワード}
$ export RELEASE_KEYSTORE_KEY_PASSWORD={鍵のパスワード}
$ ./gradlew :app:bundleRelease
```

ビルドに成功すると、 `{Module name}/build/outputs/bundle/{Variant}` フォルダに `{Module name}-{Variant}.aab` が生成されます。
私の場合、 `app/build/outputs/bundle/release` フォルダに `app-release.aab` が生成されました。

#### エラー：キーストアファイルが対象フォルダにない

以下のエラーが発生する場合、キーストアのファイルが対象モジュールのルートフォルダに存在しません。
配置しているか確認してください。

```shell-session
> Keystore file '.../{Project name}/app/release.keystore' not found for
 signing config 'release'.
```

#### エラー：キーストアまたは鍵のパスワードを設定していない

以下のエラーが発生する場合、キーストアまたは鍵のパスワードの環境変数をエクスポートしていません。
環境変数を使う場合は忘れずにエクスポートしてください。

```shell-session
* What went wrong:
Execution failed for task ':app:signReleaseBundle'.
> A failure occurred while executing http://com.android.build.gradle.internal.tasks.Workers$ActionFacade
   > kotlin.KotlinNullPointerException (no error message)
```

## おわりに

これでコマンドラインでAABを生成することができました！
あとはPlay Consoleにアップロードすることで、テストやリリースを行えます。

## 参考リンク

- [Android App Bundle について  |  Android デベロッパー  |  Android Developers](https://developer.android.com/guide/app-bundle?hl=ja)
- [コマンドラインからアプリをビルドする  |  Android デベロッパー  |  Android Developers](https://developer.android.com/studio/build/building-cmdline?hl=ja)