[Unity] モバイル VR の APK 出力を Docker で構築するための手順

# はじめに

最近 Go/GearVR 向けに作っていた VR アプリを Quest にも対応させる機会がありました :sunglasses: 

その際 Go/GearVR と Quest の APK をそれぞれ出力する時に、
Go/GearVR と Quest の設定を切り替える度に `ApplicationID` を変更したり、
外部サービスのコンフィグファイル差し替え作業等が必要になることが判明しました。。:pick:

更に改修の度に検証やリリース用として、Go/GearVR と Quest 用含め、
計6つも APK を出力することが必要になりました。。:persevere: 

仕方なく最初は手作業で APK を 6つ出力していたのですが、
それだけで 20分近くも時間がかかるようになってしまいました。。 :sob: 
(しかも手作業だと誤った APK を出力してしまう時もあり、そうなるとやり直し。。:x: 

流石にこのままだとマズイと感じたため、まずは APK 出力を Docker で自動化しました :robot: 
Docker を採用した理由は様々な CI 環境に乗せようと思った時に好都合だからです :whale: 

最終的に 6つの APK 出力にかかる時間は **20分 -> 10分** で完了するようになり、
誤った内容の APK が出力されることも無くなりました :thumbsup: 

本記事では APK 出力を Docker で自動化するために行った対策手順について書いていきます :pencil: 

# 動作環境

- Unity 2018.4.9f1
- Docker 19.03.5

# アプリケーションの設定値を GUI で変更出来るようにする

まずは自動化を進めるにあたって、
`EditorWindow` で GUI でアプリケーションの設定値が編集が出来るようにします。
GUI で設定した値は `ScriptableObject` で管理出来るようにします。

## `ScriptableObject` でアプリケーションの設定値を管理可能にする

今回は下記をアプリケーションの設定値として定義します :arrow_down: 

- EnvironmentType (Enum): `STAGING` `PRODUCTION` `RELEASE`
- DeviceType (Enum): `GEAR_VR_AND_GO` `QUEST`

`EnvironmentType` は実行環境 (検証 / 本番 / リリース) が指定できる項目となります。
`DeviceType` は実行端末 (GearVR or Go / Quest) が指定できる項目となります。

実行環境や実行端末に応じて API のリクエスト先変更したり、
クレデンシャルを変更したり出来るようになる想定で用意しました :raised_hand: 

また、アプリケーションの設定値を保存する際は、
実行端末を表す `DeviceType` の値に応じて Oculus Platform の App ID も変更するようにします :raised_hand: 

早速アプリケーション設定値を管理するための `ScriptableObject` を作成します :writing_hand: 

```csharp:Assets/Scripts/ApplicationSetting.cs
using System.IO;
using UnityEngine;
using UnityEditor;

// アプリケーションの設定値を管理するための ScriptableObject
public class ApplicationSetting : ScriptableObject
{
    // 実行端末 (GearVR or Go / Quest) の Enum
    public enum DeviceType
    {
        GEAR_VR_AND_GO, QUEST
    }

    // 実行環境 (検証 / 本番 / リリース) の Enum
    public enum EnvironmentType
    {
        STAGING, PRODUCTION, RELEASE
    }

    // 実行端末 (GearVR or Go / Quest) を設定するための変数
    // 値の変更は Editor 上からのみ許可する
    [SerializeField]
    private DeviceType _device;
    public DeviceType Device
    {
        get { return _device; }
#if UNITY_EDITOR
        set { _device = value; }
#endif
    }

    // 実行環境 (検証 / 本番 / リリース) を設定するための変数
    // 値の変更は Editor 上からのみ許可する
    [SerializeField]
    private EnvironmentType _environment;
    public EnvironmentType Environment
    {
        get { return _environment; }
#if UNITY_EDITOR
        set { _environment = value; }
#endif
    }

    // ==========================================================
    // Editor 内でしか利用しない定数や関数群
#if UNITY_EDITOR
    // アプリケーションの設定値の保存先
    const string ASSET_FILE_PATH = "Assets/Resources/ApplicationSetting.asset";

    // GearVR もしくは Oculus Go 用の App ID
    const string GEAR_VR_AND_GO_APPID = "1111111111111111";

    // Quest 用の App ID
    const string QUEST_APPID = "2222222222222222";

    public static ApplicationSetting ReadFromEditor()
    {
        return AssetDatabase.LoadAssetAtPath<ApplicationSetting>(ASSET_FILE_PATH);
    }

    // アプリケーションの設定値を保存するために使用する関数
    public void Save()
    {
        // アプリケーション設定を保存するファイルが存在しなければ新たに生成する
        WriteFileIfNotExists();

        // Device 変数の値を元に適切な AppID を Oculus Platform に設定する
        SetAppID();

        // Unity の Inspector からの設定変更を許可しない
        this.hideFlags = HideFlags.NotEditable;

        // ScriptableObject に変更があったことを記録する
        EditorUtility.SetDirty(this);

        // ScriptableObject の変更内容を保存する
        AssetDatabase.SaveAssets();

        // ScriptableObject をインポートし直す
        AssetDatabase.Refresh();
    }

    // アプリケーションの設定値を保存するためのファイルを生成する関数
    void WriteFileIfNotExists()
    {
        // 既にファイルが存在していれば処理を中断する
        if (File.Exists(ASSET_FILE_PATH)) return;

        // ASSET_FILE_PATH で指定された場所にファイルの実体を作成する
        string directory = Path.GetDirectoryName(ASSET_FILE_PATH);
        if (!Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }
        AssetDatabase.CreateAsset(this, ASSET_FILE_PATH);
    }

    // Device 変数の値を元に Oculus Platform の App ID の値を設定するために使用する関数
    void SetAppID()
    {
        string appId = "";
        switch (this.Device)
        {
            case DeviceType.GEAR_VR_AND_GO:
                appId = GEAR_VR_AND_GO_APPID;
                break;
            case DeviceType.QUEST:
                appId = QUEST_APPID;
                break;
        }
        Oculus.Platform.PlatformSettings.MobileAppID = appId;
    }
#endif
}
```

アプリケーションの設定値を管理するスクリプトが出来たので、
次は `EditorWindow` から設定が変更出来るようにします :thumbsup: 

## `EditorWindow` を利用して GUI からアプリケーションの設定値を変更可能にする

作成した `ScriptableObject` の値を `EditorWindow` から変更出来るようにします :gear: 

```csharp:Assets/Editor/ApplicationSettingWindow.cs
using UnityEngine;
using UnityEditor;

// アプリケーションの設定値を編集できる画面を EditorWindow で作成する
public class ApplicationSettingWindow : EditorWindow
{
	// アプリケーションの設定値を管理するための変数
	private ApplicationSetting applicationSetting;

	// メニューの Window -> Application Setting からアクセス可能にする
	[MenuItem("Window/Application Setting")]
	public static void Create()
	{
		// アプリケーションの設定値を編集できる画面を作成する
		GetWindow<ApplicationSettingWindow>("Application Setting");
	}

	private void OnEnable()
	{
		// 画面が有効化され次第、アプリケーション設定値をファイルから読み込む
		// ファイルから設定値が読み込めなかった場合は null が設定される
		applicationSetting = ApplicationSetting.ReadFromEditor();
	}

	private void OnGUI()
	{
		// applicationSetting が正しく初期化されていなかったら、
		// 新しくインスタンスを生成して applicationSetting に代入する
		if (applicationSetting == null)
		{
			applicationSetting = ScriptableObject.CreateInstance<ApplicationSetting>();
		}

		// 画面に縦並びに実行環境設定用の選択リスト、実行端末設定用の選択リスト、Save ボタン (アプリケーション設定保存用ボタン) を配置する
		using (new GUILayout.VerticalScope())
		{
            // 実行環境設定用の選択リストの値が変更される度に実行環境の値を設定する
			applicationSetting.Environment =
				(ApplicationSetting.EnvironmentType)EditorGUILayout.EnumPopup("Environment", applicationSetting.Environment);

            // 実行端末設定用の選択リストの値が変更される度に実行端末の値を設定する
            applicationSetting.Device =
				(ApplicationSetting.DeviceType)EditorGUILayout.EnumPopup("Device", applicationSetting.Device);

            // Save ボタンをクリックすることで、アプリケーションの設定値を保存 / 更新する
            if (GUILayout.Button("Save"))
			{
				applicationSetting.Save();
			}
		}
	}
}
```

上記スクリプトを `Assets/Editor/ApplicationSettingWindow.cs` に配置すると、
Unity メニューの `Window -> Application Setting` から、
アプリケーション設定値の編集画面に遷移することが出来るようになっているはずです:sunny:

<img width="713" alt="スクリーンショット 2020-02-02 16.01.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/4c96dc41-76bd-c827-e1d4-1c99f95d18be.png">

また、編集画面に遷移後、`Environment` と `Device` を適当な値に設定してから
`Save` ボタンをクリックすると `Resources` フォルダにアプリケーション設定値を保存するためのファイルが生成されます :white_check_mark: 

アプリケーション設定値を保存するためのファイルが生成されたのを確認した後、
編集画面を開き直すと最後に更新した値が反映されていることが確認出来ます :thumbsup: 

<img width="1675" alt="スクリーンショット 2020-02-02 16.12.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/f2dd79ce-e385-6b83-be1e-e3d6ba647e63.png">

更に Unity メニューの `Oculus -> Platform -> Edit Settings` の `Application ID` を見ると、
設定した `Device` に応じて値が変化している様子が確認出来ます :eyeglasses:

<img width="659" alt="スクリーンショット 2020-02-02 16.28.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/2a30f3eb-1a57-0690-c320-7740a8a74004.png">

# Unity バッチモードで APK 出力が出来るようにする

次は Docker で APK 出力が出来るようにするために、
Unity バッチモード経由で APK 出力出来るようにします :hammer_pick: 

APK の出力はプログラム経由でも実行することが可能です :arrow_down:

```csharp:Assets/Editor/ApkBuilder.cs
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System;
using System.IO;

public class ApkBuilder
{
    // 第一引数で指定したコマンドライン引数の値の 1つ後ろの値を取得するための関数
    // 例: "-device QUEST" というパラメータの QUEST を取得したい場合は key に "-device" 文字列を指定する。
    static string GetValueFromCommandLineArgs(string key) {
        string[] args = System.Environment.GetCommandLineArgs();
        for (int i = 0; i < args.Length; i++) {
            if (key == args[i])
                return args[i + 1];
        }
        return null;
    }

	public static void Build()
	{
        // Unity バッチモードで関数を実行する際は keystore の設定が無いので、
        // コマンドライン引数から各種情報について渡すようにする必要がある
        if (PlayerSettings.Android.keystoreName.Length == 0 ||
            PlayerSettings.Android.keystorePass.Length == 0 ||
            PlayerSettings.Android.keyaliasName.Length == 0 ||
            PlayerSettings.Android.keyaliasPass.Length == 0)
        {
            PlayerSettings.Android.keystoreName = GetValueFromCommandLineArgs("-keystoreName");
            PlayerSettings.Android.keyaliasName = GetValueFromCommandLineArgs("-keyaliasName");
            PlayerSettings.Android.keystorePass = GetValueFromCommandLineArgs("-keystorePass");
            PlayerSettings.Android.keyaliasPass = PlayerSettings.Android.keystorePass;

            if (PlayerSettings.Android.keystoreName == null ||
                PlayerSettings.Android.keyaliasName == null ||
                PlayerSettings.Android.keystorePass == null)
            {
                Debug.LogError("Please set android keystore settings.");
                return;
            }
        }

        // Build Settings で設定シーンを全てビルドに含める
        EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
		List<string> scenePathList = new List<string>();

		foreach (EditorBuildSettingsScene scene in scenes)
		{
			scenePathList.Add(scene.path);
		}

		var applicationSetting = ApplicationSetting.ReadFromEditor();

		string device = applicationSetting.Device.ToString().ToLower();
		string environment = applicationSetting.Environment.ToString().ToLower();

		BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
		buildPlayerOptions.scenes = scenePathList.ToArray();

        // プロジェクト直下の build フォルダに quest_production_20200202.apk のようなファイル名の APK を出力する
		DateTime currentDate = DateTime.Now;
		string dateString = $"{currentDate.Year}{currentDate.Month}{currentDate.Day.ToString("D2")}";

		string outputDirectory = "build";
		if (!Directory.Exists(outputDirectory))
		{
			Directory.CreateDirectory(outputDirectory);
		}

		buildPlayerOptions.locationPathName = $"{outputDirectory}/{device}_{environment}_{dateString}.apk";
		buildPlayerOptions.target = BuildTarget.Android;
		buildPlayerOptions.options = BuildOptions.None;

		applicationSetting.Save();

		BuildPipeline.BuildPlayer(buildPlayerOptions);
	}
}
```

`Assets/Editor/ApkBuilder.cs` スクリプト内の `Build` 関数は
キーストアが既に存在している前提の作りとなっているため、
まだキーストアを作っていない方は [こちらの手順](https://qiita.com/ptkyoku/items/3fb1ae542ff04423c781) に従って、
予めキーストアファイルとエイリアスの作成を行っておきましょう :raising_hand:

また上記の `ApkBuilder.Build` 関数は Unity バッチモードからも実行出来ますが、
Editor 上からも実行出来るようになっています。

Editor 上からも APK 出力が確認できるようになっていると、
本当に正確な APK 出力が行われるかデバッグを行う際に便利です :thumbsup: 

そこで、早速 Editor 上からも実行できるように、
`Assets/Editor/ApplicationSettingWindow.cs` に `Build` ボタンを配置して、
ボタンをクリックしたら APK 出力出来るようにしてみましょう :dash: 

```csharp:Assets/Editor/ApplicationSettingWindow.cs
using UnityEngine;
using UnityEditor;

// アプリケーションの設定値を編集できる画面を EditorWindow で作成する
public class ApplicationSettingWindow : EditorWindow
{
    // アプリケーションの設定値を管理するための変数
    private ApplicationSetting applicationSetting;

    // メニューの Window -> Application Setting からアクセス可能にする
    [MenuItem("Window/Application Setting")]
    public static void Create()
    {
        // アプリケーションの設定値を編集できる画面を作成する
        GetWindow<ApplicationSettingWindow>("Application Setting");
    }

    private void OnEnable()
    {
        // 画面が有効化され次第、アプリケーション設定値をファイルから読み込む
        // ファイルから設定値が読み込めなかった場合は null が設定される
        applicationSetting = ApplicationSetting.ReadFromEditor();
    }

    private void OnGUI()
    {
        // applicationSetting が正しく初期化されていなかったら、
        // 新しくインスタンスを生成して applicationSetting に代入する
        if (applicationSetting == null)
        {
            applicationSetting = ScriptableObject.CreateInstance<ApplicationSetting>();
        }

        // 画面に縦並びに実行環境設定用の選択リスト、実行端末設定用の選択リスト、Save ボタン (アプリケーション設定保存用ボタン) を配置する
        using (new GUILayout.VerticalScope())
        {
            // 実行環境設定用の選択リストの値が変更される度に実行環境の値を設定する
            applicationSetting.Environment =
                (ApplicationSetting.EnvironmentType)EditorGUILayout.EnumPopup("Environment", applicationSetting.Environment);

            // 実行端末設定用の選択リストの値が変更される度に実行端末の値を設定する
            applicationSetting.Device =
                (ApplicationSetting.DeviceType)EditorGUILayout.EnumPopup("Device", applicationSetting.Device);

            // Save ボタンをクリックすることで、アプリケーションの設定値を保存 / 更新する
            if (GUILayout.Button("Save"))
            {
                // Unity の Inspector からの設定変更を許可しない
                applicationSetting.hideFlags = HideFlags.NotEditable;

                // ScriptableObject に変更があったことを記録する
                EditorUtility.SetDirty(applicationSetting);

                applicationSetting.Save();
            }

            // Build ボタンをクリックし、ApkBuilder.Build 関数を実行することで、
            // 現在のアプリケーション設定内容を元に APK をプロジェクト直下の build フォルダに生成する
            if (GUILayout.Button("Build"))
            {
                ApkBuilder.Build();
            }
        }
    }
}
```

これで Unity メニューの `Window -> Application Setting` を開いた際に、
`Build` ボタンが画面の最下部に表示されるようになったはずです :thumbsup: 

試しに `Build` ボタンをクリックして APK 出力を行ってみましょう :arrow_down: 

<img width="324" alt="スクリーンショット 2020-02-02 21.38.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/6d953777-7abf-392b-4a67-4dbfee17776b.png">
<img width="623" alt="スクリーンショット 2020-02-02 21.40.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/05312b5b-7877-c0ab-7484-fe89165f1ddf.png">

下記のコマンドをターミナルで入力すると、
`build` フォルダ内に APK が再度出力されること確認出来ると思います :raised_hand: 
(APK ファイルが上書きされて更新日時が新しくなっているはず :sparkles:

```shell
# Windows の場合
"C:\Program Files\Unity\Editor\Unity.exe" \
-quit -batchmode -projectPath ~/Desktop/CISample \
-executeMethod ApkBuilder.Build \
-keystoreName ~/Desktop/CISample.keystore -keyaliasName cisample \
-keystorePass CISample

# Mac の場合
/Applications/Unity/Hub/Editor/2018.4.9f1/Unity.app/Contents/MacOS/Unity \
-quit -batchmode -projectPath ~/Desktop/CISample \
-executeMethod ApkBuilder.Build \
-keystoreName ~/Desktop/CISample.keystore -keyaliasName cisample \
-keystorePass CISample
```

あとは Unity バッチモード経由で様々な設定の APK 出力が出来るように、
`Assets/Editor/ApkBuilder.cs` を改修していきます :crab: 

## Unity バッチモードで引数を元に APK 出力の設定が出来るようにする

具体的には `Assets/Editor/ApkBuilder.cs` に下記のような改修を行います :arrow_down: 

```csharp:Assets/Editor/ApkBuilder.cs
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System;
using System.IO;
using DeviceType = ApplicationSetting.DeviceType;
using EnvironmentType = ApplicationSetting.EnvironmentType;

public class ApkBuilder
{
    // 第一引数で指定したコマンドライン引数の値の 1つ後ろの値を取得するための関数
    // 例: "-device QUEST" というパラメータの QUEST を取得したい場合は key に "-device" 文字列を指定する。
    static string GetValueFromCommandLineArgs(string key) {
        string[] args = System.Environment.GetCommandLineArgs();
        for (int i = 0; i < args.Length; i++) {
            if (key == args[i])
                return args[i + 1];
        }
        return null;
    }

	public static void Build()
	{
        var defaultApplicationSetting = ApplicationSetting.ReadFromEditor();
        var applicationSetting = ApplicationSetting.ReadFromEditor();

        // Unity バッチモードで実行時にコマンドライン引数の情報を元に
        // Device と Environment の設定を変更出来るようにする
        string deviceStringArg = GetValueFromCommandLineArgs("-device");
        string environmentStringArg = GetValueFromCommandLineArgs("-environment");

        if (deviceStringArg != null && environmentStringArg != null)
        {
            applicationSetting.Device = (DeviceType)Enum.Parse(typeof(DeviceType), deviceStringArg);
            applicationSetting.Environment = (EnvironmentType)Enum.Parse(typeof(EnvironmentType), environmentStringArg);
        }

        // Unity バッチモードで関数を実行する際は keystore の設定が無いので、
        // コマンドライン引数から各種情報について渡すようにする必要がある
        if (PlayerSettings.Android.keystoreName.Length == 0 ||
            PlayerSettings.Android.keystorePass.Length == 0 ||
            PlayerSettings.Android.keyaliasName.Length == 0 ||
            PlayerSettings.Android.keyaliasPass.Length == 0)
        {
            PlayerSettings.Android.keystoreName = GetValueFromCommandLineArgs("-keystoreName");
            PlayerSettings.Android.keyaliasName = GetValueFromCommandLineArgs("-keyaliasName");
            PlayerSettings.Android.keystorePass = GetValueFromCommandLineArgs("-keystorePass");
            PlayerSettings.Android.keyaliasPass = PlayerSettings.Android.keystorePass;

            if (PlayerSettings.Android.keystoreName == null ||
                PlayerSettings.Android.keyaliasName == null ||
                PlayerSettings.Android.keystorePass == null)
            {
                Debug.LogError("Please set android keystore settings.");
                return;
            }
        }

        // Build Settings で設定シーンを全てビルドに含める
        EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
		List<string> scenePathList = new List<string>();

		foreach (EditorBuildSettingsScene scene in scenes)
		{
			scenePathList.Add(scene.path);
		}

		BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
		buildPlayerOptions.scenes = scenePathList.ToArray();

        // プロジェクト直下の build フォルダに quest_production_20200202.apk のようなファイル名の APK を出力する
		DateTime currentDate = DateTime.Now;
		string dateString = $"{currentDate.Year}{currentDate.Month}{currentDate.Day.ToString("D2")}";

		string outputDirectory = "build";
		if (!Directory.Exists(outputDirectory))
		{
			Directory.CreateDirectory(outputDirectory);
		}

        string device = applicationSetting.Device.ToString().ToLower();
        string environment = applicationSetting.Environment.ToString().ToLower();

        buildPlayerOptions.locationPathName = $"{outputDirectory}/{device}_{environment}_{dateString}.apk";
		buildPlayerOptions.target = BuildTarget.Android;
		buildPlayerOptions.options = BuildOptions.None;

		applicationSetting.Save();

		BuildPipeline.BuildPlayer(buildPlayerOptions);

        // Unity プロジェクトで元々設定していた内容にアプリケーション設定を戻す
        defaultApplicationSetting.Save();
	}
}
```

これで Unity バッチモードで `ApkBuilder.Build` 関数実行時に、
`-device` と `-environment` を指定した際に、
明示的にアプリケーション設定を指定した状態で APK を出力出来るようになりました :laughing:

試しに Unity バッチモードで `-device` と `-environment` も引数に指定して APK 出力を行います :hammer: :arrow_down: 

```shell
/Applications/Unity/Hub/Editor/2018.4.9f1/Unity.app/Contents/MacOS/Unity \
-quit -batchmode -projectPath ~/Desktop/CISample \
-executeMethod ApkBuilder.Build \
-keystoreName ~/Desktop/CISample.keystore -keyaliasName cisample \
-keystorePass CISample \
-device GEAR_VR_AND_GO -environment RELEASE
```

すると `-device` と `-environment` で指定した内容で APK が出力されているはずです :thumbsup: 

<img width="746" alt="スクリーンショット 2020-02-02 23.05.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/b710d2fb-4c71-cb74-e9f8-98b8c1d07674.png">

次は Docker コンテナで Unity バッチモードが実行できるようにします :whale: 

# Docker コンテナで Unity のバッチモード実行が出来るようにする

まずは Unity の Docker イメージを pull します :arrow_down: 
私は 2018.4.9f1 を使用していたので、tag が `2018.4.9f1-android` のイメージを pull しています :raised_hand: 

```shell
docker pull gableroux/unity3d:2018.4.9f1-android
```

Docker イメージのダウンロードが完了した後は、
下記コマンドで Docker コンテナを立ち上げて対話モードを起動します :electric_plug: 

```shell
# bash の場合
docker run -it --rm  -e "UNITY_USERNAME=<Unity ID のユーザ名>" \
-e "UNITY_PASSWORD=<Unity ID のパスワード>" -e "TEST_PLATFORM=linux" \
-e "WORKDIR=/root/project" -e "$(pwd):/root/project" \
gableroux/unity3d:2018.4.9f1-android bash

# fish の場合
eval docker run -it --rm  -e "UNITY_USERNAME=<Unity ID のユーザ名>" \
-e "UNITY_PASSWORD=<Unity ID のパスワード>" -e "TEST_PLATFORM=linux" \
-e "WORKDIR=/root/project" -e "(pwd):/root/project" \
gableroux/unity3d:2018.4.9f1-android bash
```

対話モードが正常に起動できたら下記コマンドで、
ライセンスのアクティベーションを要求するための xml を出力します :outbox_tray: 

```shell
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
/opt/Unity/Editor/Unity -logFile -batchmode \
-username "$UNITY_USERNAME" -password "$UNITY_PASSWORD"

# ...
# 下記の Posting という出力の後ろにある xml を、
# Unity_v<バージョン>.alf という名前でファイルに保存する
LICENSE SYSTEM [202022 9:57:44] Posting <?xml version="1.0" encoding="UTF-8"?><root><SystemInfo><IsoCode>en</IsoCode><UserName>(unset)</UserName><OperatingSystem>...</MachineBindings><UnityVersion Value="2018.4.9f1" /></License></root>
# ...
```

私は Unity のバージョン `2018.4.9f1` を使用しているので、
`Unity_v2018.4.9f1.alf` というファイル名で保存しました :floppy_disk: 

その後、https://license.unity3d.com/manual へアクセスします :earth_americas: 

するとライセンスのアクティベーションを要求するためにファイルをアップロードするよう促されるので、
先程保存した `Unity_v2018.4.9f1.alf` というファイルをアップロードします :arrow_up: 
<img width="1230" alt="スクリーンショット 2020-02-03 0.17.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/040560b0-ce58-e185-b29e-3a0d036ba67d.png">
アップロードして無事にアクティベーションの要求が成功すると、
`Unity Plus or Pro` と `Unity Personal Edition` のライセンス、
どちらをアクティベートするか聞かれるので選択して Next ボタンをクリックします :point_up_2: 
<img width="1281" alt="スクリーンショット 2020-02-03 0.20.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86070/6a4ecc99-7c16-812b-deea-0e6b8a0d358e.png">
正常に認証が完了すれば、
`Download license file` ボタンが出てくるので、クリックしてライセンスファイルをダウンロードします。
ライセンスファイルは `Unity_v2018.x.ulf` という名前でダウンロードされます :white_check_mark: 

これでようやく Docker コンテナ上で Unity バッチモードを実行する環境が整いました :thumbsup: 
早速 Unity プロジェクトビルド用の `Dockerfile` を作成します :pencil: 

```dockerfile
# Unity プロジェクトのバージョンと合わせて Unity バージョンは 2018.4.9f1 を使用する
# また Android プラットフォーム向けのビルドが行えるイメージを引っ張ってくる
FROM gableroux/unity3d:2018.4.9f1-android
LABEL maintainer="Admin <admin@nikaera.com>"

# Unity ライセンスファイルやキーストア、プロジェクトファイルを Docker イメージに内包する
COPY ./Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf
COPY ./CISample.keystore /root/CISample.keystore
COPY ./unity /root/unity

# APK 出力のための関数への引数として DEVICE と APP_ENV という環境変数で 
CMD /opt/Unity/Editor/Unity \
    -quit -batchmode -nographics -logFile -projectPath /root/unity \
    -executeMethod ApkBuilder.Build \
    -keystoreName /root/CISample.keystore -keyaliasName cisample \
    -keystorePass CISample \
    -device $DEVICE -environment $APP_ENV
```

また Docker 導入にあたってプロジェクトのフォルダ構成は下記のようになっております :arrow_down: 

```
.
├── CISample.keystore	# Android ビルド時に利用するキーストアファイル
├── Dockerfile		# Docker イメージをビルド際に使用するファイル
├── Unity_v2018.x.ulf	# Unity のライセンスファイル
└── unity		# Unity プロジェクトフォルダを unity フォルダに移行する
    ├── Assembly-CSharp-Editor.csproj
    ├── Assembly-CSharp.csproj
    ├── Assets
    ├── CISample.sln
    ├── Library
    ├── Logs
    ├── Oculus.VR.Editor.csproj
    ├── Oculus.VR.Scripts.Editor.csproj
    ├── Oculus.VR.csproj
    ├── Packages
    ├── ProjectSettings
    ├── build
    └── obj
```

`Dockerfile` の内容を元にプロジェクトルートでターミナルから下記コマンドでイメージを作成します :hammer: 

```shell
docker build -t nikaera/cisample .
```

正常にイメージ作成出来たら試しに実際に Docker コンテナで APK を出力してみます :outbox_tray: 

```
# APK 出力フォルダをホストと共有しておくことで出力した APK が参照出来るようにする
# 環境変数 DEVICE に QUEST、環境変数 APP_ENV に RELEASE を指定することで
# Quest のリリース版 APK を出力する。
docker run -v ~/Desktop/build:/root/unity/build --rm \
--env DEVICE=QUEST --env APP_ENV=RELEASE nikaera/cisample
```

正常に APK 出力出来ればデスクトップの build フォルダ内に `quest_release_<日付>.apk` が出来ているはずです :thumbsup: :tada: 

# Docker コンテナで全ての APK 出力を行う

ここまできたら後はスクリプト等で一気に全パターンの APK を出力します:hammer: 
まずは全 APK 出力を Docker コンテナで行うためのシェルスクリプトを作成します :arrow_down: 

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

docker run -v $1:/root/unity/build --rm --env DEVICE=GEAR_VR_AND_GO --env APP_ENV=STAGING nikaera/cisample

docker run -v $1:/root/unity/build --rm --env DEVICE=GEAR_VR_AND_GO --env APP_ENV=PRODUCTION nikaera/cisample

docker run -v $1:/root/unity/build --rm --env DEVICE=GEAR_VR_AND_GO --env APP_ENV=RELEASE nikaera/cisample

docker run -v $1:/root/unity/build --rm --env DEVICE=QUEST --env APP_ENV=STAGING nikaera/cisample

docker run -v $1:/root/unity/build --rm --env DEVICE=QUEST --env APP_ENV=PRODUCTION nikaera/cisample

docker run -v $1:/root/unity/build --rm --env DEVICE=QUEST --env APP_ENV=RELEASE nikaera/cisample
```

シェルスクリプトを用意次第、下記コマンドで一気に APK を出力します :outbox_tray: 

```bash
 bash build.sh ~/Desktop/build
```

すると全実行環境及び全実行端末の APK が、
デスクトップの `build` フォルダに生成されていること確認できるはずです :thumbsup: :thumbsup: 

# おわりに

今回は Docker コンテナで Unity バッチモードを動かして、
モバイル VR 向けの APK を一括で出力出来るようにしてみました。

Docker コンテナ上で Unity を動かすための
ライセンス発行処理周りの作業が面倒なので出来れば自動化したい。。:arrows_counterclockwise:

# 参考リンク

[[Unity]KeyStore作成メモー](https://qiita.com/ptkyoku/items/3fb1ae542ff04423c781)
[gableroux/unity3d](https://gitlab.com/gableroux/unity3d)
[Unity の Android ビルドを CLI からおこなう](https://nekonenene.hatenablog.com/entry/2019/05/20/030642)
[【Unity】GitHub Actions v2でUnity Test Runnerを走らせて、結果をSlackに報告する【入門】](https://qiita.com/pCYSl5EDgo/items/690dd56ffb0fcf64e70b)