Unity + DockerでUnityプロジェクトからAARファイルを自動生成

# はじめに

*UnityのSceneをAndroidのSubView(FrameLayout)として埋め込むまで*
https://qiita.com/_nonono/items/253aa15d6027ecc8ad66
このような記事があり、AndroidにUnityのシーンをFrameLayoutに埋め込むことができます(便利

**Unityで開発せずビルド成果物(AARファイル)のみ必要な場合**
1.指定のバージョンのUnityをインストール
2.Unityを立ち上げAndroidプロジェクトをExport
3.Android Studioでビルド
上の3つの作業を手作業で行うので一苦労だと思います
Unity、Android Studioのバージョンを意識したりしなければならいので良い方法が必要です

「なんとか良い方法を！」と思って `Unityの入ったDockerイメージを使ってAARの生成を自動化` してみたので、作業を下に記していきます

# TL;DR
いきなり一連の作業をスクリプトにできないので、dockerの中で作業を1つずつして、最後にスクリプトにまとめます
## やったこと
1. `docker pull gableroux/unity3d`
2. Dockerコンテナに入りライセンスファイルを用意
3. ライセンスファイルをコピー
4. ビルド用クラスを作成
5. Unityプロジェクトをビルド
6. build.gradleとAndroidManifest.xmlを編集
7. GradleでAndroidプロジェクトをビルド
8. build.shを書いて完全自動化

## 実際の環境
・MacBook Pro (15-inch, 2016) macOS 10.13.6（17G10007）
・docker desktop ver. 2.1.0.5(40693)

# 1. Unityの入ったDockerイメージをpullする
まずは、Unityの入っているDockerイメージをインターネットから落としてきます

次のコマンドを実行すると最新版のコンテナが自分のローカルに保存されます

```bash:Dockerイメージをpull
docker pull gableroux/unity3d
```

しかし、このイメージではAndroidプロジェクトを生成/ビルドができないので、`Unityバージョン`とUnityプロジェクトの`ビルドターゲット`を指定しなければなりません。

この記事では、 `Unity 2018.4.5f1` でビルドターゲットを`Android` とするので、Dockerイメージのtagに`2018.4.5f1-android`を指定します
※目的のバージョンのイメージが無い場合があるので、 [DockerHub](https://hub.docker.com/r/gableroux/unity3d/tags) のページで確認しておきましょう

```bash:目的のDockerイメージをpull
docker pull gableroux/unity3d:2018.4.5f1-android
```

`docker images`を実行するとpullしてきたイメージが確認できます

```bash:DockerImageの確認
cha84rakanal$ docker images
REPOSITORY            TAG                  IMAGE ID            CREATED             SIZE
gableroux/unity3d     2018.4.5f1-android   3cc6afa5c2bd        3 months ago        6.7GB
```

`docker run -it {image name} bash` を実行すればイメージからコンテナが作成され、Dokcer内のターミナルに接続できる
なので、接続して`Unity`と`Android SDK`があるかを確認しておきます

```bash:UnityとAndroidSDKの確認
cha84rakanal$ docker run -it --rm gableroux/unity3d:2018.4.5f1-android bash
root@7462f939f4d0:/# echo $ANDROID_HOME
/opt/android-sdk-linux
root@7462f939f4d0:/# ls /opt/
android-sdk-linux  Unity  Unity-2018.4.5f1
root@7462f939f4d0:/# 
```

# 2. Dockerコンテナに入ったUnity用のライセンスを生成する(1回のみ・手作業)
普段使うmacOSやWindowsでUnityを一番最初に起動すると次の画面が出てアクティベーションをする必要があります
![アクティベーション画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224134/79d75f8e-ccc1-0c2a-9660-47a5e29e4d1d.png)

次の作業は、DockerコンテナにインストールされているUnityのアクティベーションを行います

まず、Unityプロジェクトジェクトのディレクトリに移動してdockerコンテナを立ち上げます
※ **パスワードを平打ちするので気をつけましょう**

```bash:Dockerコンテナの立ち上げ
cd /path/to/project
docker run -it --rm \
-e "UNITY_USERNAME=username@example.com" \
-e "UNITY_PASSWORD=example_password" \
-e "TEST_PLATFORM=linux" \
-e "WORKDIR=/root/project" \
-v "$(pwd):/root/project" \
gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \
bash
```

次に、Dockerコンテナ内で以下のコマンドを実行します

```bash:アクティベーションをする
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity \
-logFile /dev/stdout \
-batchmode \
-username "$UNITY_USERNAME" -password "$UNITY_PASSWORD"
```

すると、出力に次のようなXMLが表示されるので、表示されたXMLを `unity3d.alf` というファイル名で保存します

```bash:ターミナルでの出力
LICENSE SYSTEM [2017723 8:6:38] Posting <?xml version="1.0" encoding="UTF-8"?><root><SystemInfo><IsoCode>en</IsoCode><UserName>[...]
```

もし、XMLではなく`401`エラーが出た場合は、2段階認証を切りましょう
`Googleでサインイン`や`FaceBookでサインイン` でUnity IDを作成している場合でも `401`エラーがでるので、メールアドレスから作成したUnityIDを使いましょう

```bash:401エラー
Can't activate unity: No sufficient permissions while processing request HTTP error code 401
```

`unity3d.alf` が用意できたら https://license.unity3d.com/manual にアクセスします。アクセスしたらサイトの指示に従って`unity3d.alf`をアップロード、質問に答えていきましょう。すべて終わると`Unity_v2018.x.ulf`か`Unity_v2019.x.ulf`がダウンロードできるので保存しておきます。
この`Unity_v*.x.ulfファイル`は今後必要になるので大切に保管しておきます

プロジェクトチームの誰かがこの作業をやれば、Unityのバージョンを変えない限り、
3以降の作業で`Unity_v2018.x.ulf`を使いまわすことができます(多分

※アクティベーションの詳しい手順は [このページ](https://gitlab.com/gableroux/unity3d-gitlab-ci-example) に書いてありますが、まとめとしてこの記事にも記しています

# 3. Dockerコンテナに入ったUnityにライセンスを当てる
2の手順でダウンロードしてきたファイル`Unity_v2018.x.ulf`をUnityプロジェクトのルートディレクトにおきます

```bash:Unity_v2018.x.ulfの設置
/path/to/project
├── Assets
├── Library
├── Logs
├── Packages
├── ProjectSettings
├── README.md
└── Unity_v2018.x.ulf
```

次に、Unityプロジェクトジェクトのディレクトリに移動してdockerコンテナを立ち上げます

```bash:Dockerコンテナの立ち上げ
cd /path/to/project
docker run -it --rm \
-v "$(pwd):/root/project" \
gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \
bash
```

**ここから先の作業7.が終わるまでは、dockerコンテナ内のターミナルで`exit`をしてはいけません**

ライセンスファイルをUnityプロジェクトのルートディレクトに設置できたら、dockerコンテナ内のターミナルで次のコマンドを実行します。これでライセンスファイルの設置は完了です

```bash:Unityのライセンスファイルを設置
set -e
set -x
mkdir -p /root/.cache/unity3d
mkdir -p /root/.local/share/unity3d/Unity/
set +x
cp ~/project/Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf
```

# 4. ビルド用クラスを作成してAssets/Scripts/Editorに配置

Unityプロジェクトをコマンドラインからビルドできるように次のC#ソースを`Assets/Scripts/Editor`に設置します

```C#:ApplicationBuild.cs
using UnityEngine;
using System;
using System.Linq;

public static class ApplicationBuild {

    private static string[] GetAllScenePaths() {
        return EditorBuildSettings.scenes
            .Where(scene => scene.enabled)
            .Select(scene => scene.path)
            .ToArray();
    }

    public static void AndroidBuild() {

        string[] scenes = GetAllScenePaths();
        BuildPipeline.BuildPlayer(scenes, "./Build/", BuildTarget.Android, BuildOptions.AcceptExternalModificationsToPlayer);
        
    }

}
```

[公式ドキュメント](https://docs.unity3d.com/ja/current/ScriptReference/BuildPipeline.BuildPlayer.html) を確認すると、 ビルドのパラメータを色々設定できます。
今回は、プロジェクトのエクポートパスは`./Build` 、ビルドターゲットはAndroidなので `BuildTarget.Android` 、AndroidのビルドはExternalで行うのでオプションに `BuildOptions.AcceptExternalModificationsToPlayer` を指定しています

**BuildPipeline.BuildPlayer**
`BuildPlayer(EditorBuildSettingsScene[] levels, string locationPathName, BuildTarget target, BuildOptions options)` 

|パラメータ||
|:--|:--|
| levels | The Scenes to be included in the build. If empty, the currently open Scene will be built. Paths are relative to the project folder (Assets/MyLevels/MyScene.unity). |
| locationPathName | 成果物の保存先のパス |
| target | ビルドする BuildTarget |
| options | ビルドしたプレイヤーを実行するか、などの追加の BuildOptions |

# 5. コマンドラインからUnityプロジェクトをビルド

次のコマンドでUnityプロジェクトをコマンドラインからビルドできます

```bash:コマンドラインからUnityプロジェクトをビルド
/opt/Unity/Editor/Unity -batchmode -quit -nographics -logFile ./build.log -projectPath . -executeMethod ApplicationBuild.AndroidBuild
```

ログは `./build.log` に保存されるので、エディタ等で確認するとビルドが進んでるのがわかります
ビルド中は、ターミナルに何かこれといって表示されるものは無いです(無視できるエラーとかは出る

ビルドが完了するとログに次の行が記録されます

```text:build.log
Exiting batchmode successfully now!
```

※成果物の保存先のパス `./Build`ディレクトリがすでにあるとビルドが停止してしまうので消しておきましょう

# 6. APKではなくAARを作成するようにbuild.gradleとAndroidManifest.xmlを編集

5の作業が終わり、ビルドが成功すると、`./Build`ディレクトリにAndroidプロジェクトが生成されます

```bash:コマンドラインでのビルド後
/path/to/project
├── Assets
├── Build
│   └── {Project Name}
│       ├── build.gradle
│       ├── gradle.properties
│       ├── libs
│       ├── local.properties
│       ├── proguard-unity.txt
│       └── src
├── Library
├── Logs
├── Packages
├── ProjectSettings
├── README.md
└── Unity_v2018.x.ulf
```

デバイスにインストールする`.apk`の作成であれば、`gradle` コマンドでビルドして終了です
今回は、`.aar`の作成なので、`build.gradle`と`AndroidManifest.xml`を編集する必要があります

`build.gradle` 

- `apply plugin: 'com.android.application'` を `apply plugin: 'com.android.library'` に変更
- `applicationId 'com.project.unitytest'` を削除
- `bundle` をコメントアウト(Unity2018.3.x以降??)

```gradle:build.gradle
--- apply plugin: 'com.android.application'
+++ apply plugin: 'com.android.library'

--- applicationId 'com.project.unitytest'  

--- bundle {
+++ /*bundle {
        language {
            enableSplit = false
        }
        density {
            enableSplit = false
        }
        abi {
            enableSplit = true
        }
--- }
+++ }*/
```

`AndroidManifest.xml`

- 該当のintent-filterタグとその子をすべてコメントアウト

```xml:AndroidManifest.xml
<!--<intent-filter>-->
<!--<action android:name="android.intent.action.MAIN" />-->
<!--<category android:name="android.intent.category.LAUNCHER" />-->
<!--<category android:name="android.intent.category.LEANBACK_LAUNCHER" />-->
<!--</intent-filter>-->
```

UnityプロジェクトからAARファイルの生成を行うには、この作業も自動化しておく必要があるので、
UnityのPostProcessの機構を使って自動化しておきます

*[Unity] PostProcessでビルド後に処理を差し込む*
https://qiita.com/edo_m18/items/346439f7678218e85e69

※ **`build.gradle`の文字コードは `UTF-8` で書き出すこと、 C#のでの`UTF8Encoding`は`UTF-8 with BOM`であるので注意が必要。 `UTF-8 with BOM`の`build.gradle`を`gradle`に渡すとエラーになるので注意**
※UnityのPostProcessBuildにはこの記事ではふれません
※`build.gradle` の `bundle`のコメントアウトだが [このページ](https://github.com/f111fei/react-native-unity-view/issues/80) によれば次に示す置換でも問題ない

```gradle:build.gradle
--- bundle {
+++ splits {
        language {
---         enableSplit = false
+++         enable false
        }
        density {
---         enableSplit = false
+++         enable false
        }
        abi {
---         enableSplit = true
+++         enable true
        }
    }
```

```C#:bundle部分を処理するC#の例
build_text = build_text.Replace("bundle {", "splits {");
build_text = build_text.Replace("enableSplit = false", "enable false");
build_text = build_text.Replace("enableSplit = true", "enable true");
```

# 7. コマンドラインでAndroidプロジェクトをビルド

残る作業は`./Build`ディレクトリに生成されたAndroidプロジェクトをビルドしてAARファイルを生成するだけです
生成されたAndroidプロジェクトのディレクトリ(`build.gradle`があるディレクトリ)に移動して次のコマンドを実行するだけです

```bash:AARファイルを生成
gradle bundleDebugAar
```

```bash:AARファイルを生成
gradle bundleReleaseAar
```

デバッグとリリースがあるので必要に応じて使い分けましょう

ビルドが成功するとプロジェクトのディレクトリに`build`ディレクトリが生成されて、
その中に `{PROJECT_NAME}-debug.aar` `{PROJECT_NAME}-release.aar` が生成されます

```bash:ビルド後
/path/to/project
├── Assets
├── Build
│   └── {Project Name}
│       ├── build
│       │   └── outputs
│       │       └── aar
│       │           ├── {PROJECT_NAME}-debug.aar
│       │           └── {PROJECT_NAME}-release.aar
│       ├── build.gradle
│       ├── gradle.properties
│       ├── libs
│       ├── local.properties
│       ├── proguard-unity.txt
│       └── src
├── Library
├── Logs
├── Packages
├── ProjectSettings
├── README.md
└── Unity_v2018.x.ulf
```

# 8. 作業をシェルスクリプトにまとめる
4から7の作業をまとめて、コマンド一行でAARファイルの生成できるようにします
ライセンスファイルはUnityプロジェクトのルートディレクトリに設置して、6の作業はUnityのPostProcessBuildで自動化しておきましょう

そうして作業をまとめたスクリプトが次になります

```sh:build.sh
#!/usr/bin/env bash

set -e
set -x
mkdir -p /root/.cache/unity3d
mkdir -p /root/.local/share/unity3d/Unity/
set +x

cp ~/project/Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf

cd ~/project/ && rm -r ./Build && /opt/Unity/Editor/Unity -batchmode -quit -nographics -logFile ./build.log -projectPath . -executeMethod ApplicationBuild.AndroidBuild
cd ~/project/Build/{Project Name} && gradle bundleDebugAar
```

Unityプロジェクトのルートディレクトリに移動して、dockerコンテナ内で`build.sh`を実行します

```bash:AARの自動ビルド
cd /path/to/project
chmod 777 build.sh
docker run -it --rm \
-v "$(pwd):/root/project" \
gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \
/bin/bash -c "/root/project/build.sh"
```

# さいごに
これでコマンドを実行してビルドされるのを待つだけになりました！
これを`Bitbucket Pipeline` や `CircleCI ` 、 `GitHub Actions` とかのCIツールに載せていけるといいですね
まとめている間に、既にmacOSやWindowsにUnityがインストールされていて、かつ、Android SDKがインストールされているなら、Docker上じゃなくていいかなと思いました

# 余談
```bash:実際の出力
cha84rakanal$ time docker run -it --rm -e "WORKDIR=/root/project" -v "$(pwd):/root/project" gableroux/unity3d:2018.4.5f1-android /bin/bash -c "/root/project/build.sh"
+ mkdir -p /root/.cache/unity3d
+ mkdir -p /root/.local/share/unity3d/Unity/
+ set +x
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5007:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2495:(snd_pcm_open_noupdate) Unknown PCM default
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5007:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2495:(snd_pcm_open_noupdate) Unknown PCM default
/home/builduser/buildslave/unity/build/Editor/Platform/Linux/UsbDevices.cpp:UsbDevicesQuery

Welcome to Gradle 5.1.1!

Here are the highlights of this release:
 - Control which dependencies can be retrieved from which repositories
 - Production-ready configuration avoidance APIs

For more details see https://docs.gradle.org/5.1.1/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

> Task :help

Welcome to Gradle 5.1.1.

To run a build, run gradle <task> ...

To see a list of available tasks, run gradle tasks

To see a list of command-line options, run gradle --help

To see more detail about a task, run gradle help --task <task>

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 5s
1 actionable task: 1 executed
debugger-agent: Unable to listen on 28
Starting a Gradle Daemon (subsequent builds will be faster)

Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/5.5.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 2m 7s
21 actionable tasks: 21 executed

real    33m9.065s
user    0m0.162s
sys     0m0.178s
cha84rakanal$ 
```

`time`コマンドで実行時間を図ったら30分もビルドしてるので、dockerコンテナ内よりホスト側でビルドした方がはやいやんけw

<!--https://qiita.com/sango/items/474efb4c016a136c84ce-->