JenkinsでUnityビルドをしよう

この記事は [KLab Advent Calendar 2019](https://qiita.com/advent-calendar/2019/klab) 15日目の記事です。

# 環境
|                 |                      |
| --------------- | -------------------- |
| Unity           | 2018.3.14f1          |
| Android SDK     | 28.0.3               |
| JDK             | 1.8.0_191            |
| NDK             | r13b                 |
| Gradle          | 4.6                  |
| GradleBuildTool | 3.2.0                |
| Xcode           | Version 10.1 (10B61) |
# 概要

Jenkinsを使ってAndroidとiOSのバイナリを作成する方法を紹介します。
EditorからのBuild&Runは実行できている状態を想定しています。


# Build時に実行する関数の作成
## 共通処理
ビルドをJenkinsから実行する際に呼び出すビルド関数をC#で作成します。
実行時にJenkinsから指定したパラメーターを渡せるようにコマンド引数を解析する仕組みも用意します。

```cs:Build.cs
public class Build
{
	/// <summary>
    /// ビルド時に実行される関数
    /// </summary>
	public static void BuildProcess()
	{
		// 引数から取得したパラメータをPlayerSettingsに設定する
		BuildTarget buildTarget = BuildTarget();
        BuildTargetGroup buildTargetGroup = BuildTargetGroup();
		
        string productName = CommandLineArgs.GetValue(productName);
        PlayerSettings.bundleVersion = CommandLineArgs.GetValue(bundleVersion);
        PlayerSettings.applicationIdentifier = CommandLineArgs.GetValue(bundleIdentifier);
        bool isDevelopmentBuild = CommandLineArgs.GetValue(developmentBuild).Equals("true");

		// プラットフォームの切り替え
        EditorUserBuildSettings.SwitchActiveBuildTarget(buildTargetGroup, buildTarget);
        EditorUserBuildSettings.development = isDevelopmentBuild;

		// ビルドオプションの設定
		BuildOptions option = BuildOptions.SymlinkLibraries;
        if (isDevelopmentBuild)
        {
            option |= BuildOptions.Development | BuildOptions.ConnectWithProfiler | BuildOptions.AllowDebugging;
        }

		// ～～～～中略～～～～
		// その他必要な設定を書き換えていく 

		// ビルドの実行
		BuildPipeline.BuildPlayer(scenes, outputPath, buildTarget, option);
	}
}

/// <summary>
/// パラメータ取得用関数
/// </summary>
public static class CommandLineArgs
{
	public static string GetValue(string parameterName)
	{
		foreach (var arg in System.Environment.GetCommandLineArgs())
		{
			if (arg.StartsWith(parameterName))
			{
				var argValue = arg.Substring(parameterName.Length);
				return argValue;
			}
		}
		return null;
	}
}

```

## Androidの場合
Debug用、Release用でバイナリを分けたりする場合にはビルド時にKeyStore情報などを切り替える必要があります。
Jenkinsから指定できるようにビルドパラメーターを用意して、PlayerSettingに設定します。

```cs:Build.cs
public static void BuildProcess()
{
	// ビルドシステムをGradleに変更
	EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;

	// AAB(Android App Bundle)ビルドを有効にする
	EditorUserBuildSettings.buildAppBundle = true;

	PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, bundleIdentifier);

	// 引数から取得したパラメータをPlayerSettingsに設定する
	PlayerSettings.Android.keystoreName = CommandLineArgs.GetValue(keyStorePath);
	PlayerSettings.Android.keystorePass = CommandLineArgs.GetValue(keyStorePass);
	PlayerSettings.Android.keyaliasName = CommandLineArgs.GetValue(keyAliasName);
	PlayerSettings.Android.keyaliasPass = CommandLineArgs.GetValue(keyAliasPass);
	PlayerSettings.Android.bundleVersionCode = int.Parse(CommandLineArgs.GetValue(bundleVersionCodeParam));
	
	// Armv7など、対象にしたいアーキテクチャを指定する
	CommandLineArgs.GetValue(targetArchitectures, true).Split(',')
			.Select(x => (AndroidArchitecture)System.Enum.Parse(typeof(AndroidArchitecture), x))
			.ForEach(x => targetArchitectures |= x);
	
	PlayerSettings.Android.targetArchitectures = targetArchitectures;

	// その他必要な設定を書き換えていく 

}
```

## iOSの場合

```cs:Build.cs
public static void BuildProcess()
{
	PlayerSettings.iOS.applicationDisplayName = productName;
	PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.iOS, bundleIdentifier);
	PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK;

	string buildNumber = CommandLineArgs.GetValue(buildNumber);
	if (!string.IsNullOrEmpty(buildNumber))
	{
		PlayerSettings.iOS.buildNumber = buildNumber;
	}

	// il2cpp設定
	if (CommandLineArgs.GetValue(keyIs2Cpp).Equals("true"))
	{
		PlayerSettings.SetScriptingBackend(buildTargetGroup, ScriptingImplementation.IL2CPP);
	}

	// その他必要な設定を書き換えていく 
}
```

## ビルド結果の解析

`BuildPipeline.BuildPlayer` は戻り値としてビルドの実行結果を返します。
その結果に応じて`EditorApplication.Exit()`を呼ぶことでJenkins側に実行結果を伝えることができます。

```cs:Build.cs
public static void BuildProcess()
{
	// ビルドの実行
	var report = BuildPipeline.BuildPlayer(scenes, outputPath, buildTarget, option);
	ResultAction(report);
}

private static void ResultAction(BuildReport report)
{
	var result = report.summary.result;

	// ビルド時に発生したLogType毎のメッセージを取得
	var messages = report.steps.SelectMany(x => x.messages)
		.ToLookup(x => x.type, x => x.content);

	Debug.Log("BuildResult : " + result);

	switch (result)
	{
		case BuildResult.Succeeded:
			EditorApplication.Exit(0);
			break;
		case BuildResult.Failed:
			Debug.Log(string.Join("\n\t", messages[LogType.Error].ToArray()));
			EditorApplication.Exit(1);
			break;
		case BuildResult.Cancelled:
		case BuildResult.Unknown:
			EditorApplication.Exit(1);
			break;
	}
}
```

# ビルドJobの作成

## ビルドのパラメーター化
新規JOBを作成し、「ビルドのパラメーター化」を有効にします。
先ほど作成したビルド用関数に渡すパラメータを作成します。
Git等を使用してブランチ管理を行っている場合は、ビルド対象のブランチも指定できるようにするといいでしょう。
![qwqw.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74151/0e899d87-8254-db6c-f697-70453927e866.png)

## プリプロセッサーの切り替え
Debug用、Release用、特定の環境用など用途に応じて有効にするプリプロセッサーを切り替えたいことがあると思います。
Unityでは`Assets/mcs.rsp`ファイル内に記載する事で、有効にするプリプロセッサーを変更することができます。

```debug.rsp
-define:DEBUG_BUILD -define:HOGE_HOGE
```

```release.rsp
-define:RELEASE_BUILD -define:FUGA_FUGA
```

のように予め用途ごとの組み合わせを定義しておき、
Jenkinsのパラメーターから使用するファイルを切り替えるようにしておき
mcs.rspを書き換えるようにすることで実現できます。

```Shell
cp ../preset/${debug/release}.rsp ./Assets/mcs.rsp
```


## Androidビルド

Unityをバッチモードで実行します。
オプションの詳細は[公式リファレンス](https://docs.unity3d.com/ja/2018.3/Manual/CommandLineArguments.html)をご確認ください。

apkビルドの場合はこれで終了です。

```sh
Unity.app/Contents/MacOS/Unity -batchmode -quit -executeMethod Build.BuildProcess \
-projectPath ${Unityプロジェクトのパス} \
-buildTarget android -productName=${PRODUCT_NAME} -bundleVersion=${BUNDLE_VERSION} \
-bundleIdentifier=${BUNDLE_IDENTIFIER} -developmentBuild=${DEVELOPMENT_BUILD} \
-bundleVersionCode=${BUNDLE_VERSION_CODE} \
-keyStorePath="${KEY_STORE_PATH}" -keyStorePass="${KEY_STORE_PASS}" \
-keyAliasName="${KEY_ALIAS_NAME}" -keyAliasPass="${KEY_ALIAS_PASS}" \
-il2cppBuild=${IL2CPP} -branchName="${BRANCH_NAME}" \
-ndkRoot=${NDKのパス} \
-targetArchitectures=${ARCHITECTURES} -logFile ${WORKSPACE}/${BUILD_NUMBER}.log || echo "Unityでエラーが発生しました。${JOB_URL}/ws/${BUILD_NUMBER}.log を確認してください。"

```

### aabビルドの場合

aabファイルをビルド成果物としている場合は、Unityビルド成功後に
aabからapkを生成する必要があります。

```sh
# aabからuniversal設定でapkを吐き出す
if [ -e "${AAB_PATH}" ]; then　# ビルドしたaabファイルのパス
  java -jar ${BUNDLE_TOOL_PATH} build-apks \ # Unity2018-3-14f1/PlaybackEngines/AndroidPlayer/Tools/bundletool-all-0.6.0.jar
  --bundle="${AAB_PATH}" \
  --output="${APKS_PATH}" \ # 出力したいパス
  --ks="${KEY_STORE_PATH}" \
  --ks-pass="pass:${KEY_STORE_PASS}" \
  --ks-key-alias="${KEY_ALIAS_NAME}" \
  --key-pass="pass:${KEY_ALIAS_PASS}" \
  --universal

  unzip "${APKS_PATH}" -d "${APKS_UNZIP_PATH}"
  mv "${APKS_UNZIP_PATH}/universal.apk" "${WORKSPACE}/apk/${BUILD_NUMBER}.apk"
  mv "${AAB_PATH}" "${WORKSPACE}/aab/${BUILD_NUMBER}.aab"
  rm -r "${APKS_UNZIP_PATH}"
else
  echo "AAB file not found."
  exit 1
fi
```

## iOSビルド

iOSの場合はUnityが成果物として生成するのはXcodeプロジェクトです。
Unityビルドの後にXcodeビルドを行いipaを出力します。

```sh
Unity.app/Contents/MacOS/Unity -batchmode -quit -executeMethod Build.BuildProcess \
-projectPath ${Unityプロジェクトのパス} \
-buildTarget ios -productName=${PRODUCT_NAME} -bundleVersion=${BUNDLE_VERSION} \
-bundleIdentifier=${BUNDLE_IDENTIFIER} -developmentBuild=${DEVELOPMENT_BUILD} \
-il2cppBuild=${IL2CPP} \
-buildNumber=${BUNDLE_VERSION_INTERNAL} -branchName="${BRANCH_NAME}" \
-logFile ${WORKSPACE}/${BUILD_NUMBER}.log \
 || echo "Unityでエラーが発生しました。${JOB_URL}/ws/${BUILD_NUMBER}.log を確認してください。"

# Xcode : クリーン
xcodebuild clean -project ${XCODE_PROJECT_DIR}/Unity-iPhone.xcodeproj \
-UseNewBuildSystem=NO \ # Xcode 10 からBuildSystemが変わったので従来のビルドを行う場合はUseNewBuildSystem=NOオプションが必要
-target Unity-iPhone　-configuration Release

# Xcode更新時に -sdk iphoneos の部分を書き換える必要があります
# Xcode : archiveビルド（Xcode 7.1 -> -sdk iphoneos9.1）
# Xcode : archiveビルド（Xcode 8.3.3 -> -sdk iphoneos10.3）
# Xcode : archiveビルド(Xcode 9.0 -> -sdk iphoneos11.0)
# Xcode : archiveビルド(Xcode 10.1 -> -sdk iphoneos12.1)
# Xcode 10 からBuildSystemが変わったので従来のビルドを行う場合はUseNewBuildSystem=NOオプションが必要
xcodebuild -scheme Unity-iPhone archive \
-project ${XCODE_PROJECT_DIR}/Unity-iPhone.xcodeproj \
-UseNewBuildSystem=NO \
-IDEBuildingContinueBuildingAfterErrors=YES \
-archivePath ${アーカイブの保存先} \
-sdk iphoneos12.1 \
-configuration Release build \
CODE_SIGN_IDENTITY="${CODE_SIGNING_IDENTIFIER}" \
PROVISIONING_PROFILE="${PUUID}" \ # 使用するプロヴィジョニングプロファイルのUUID
ENABLE_BITCODE=No \
CLANG_LINK_OBJC_RUNTIME=No

# Xcode : ipaビルド
xcodebuild -UseNewBuildSystem=NO -exportArchive \
-archivePath ${アーカイブのパス} \
-exportPath ${ipaの保存先} \
-exportOptionsPlist ${plistファイルのパス}

```

# さいごに

実際にはプロジェクト毎に色々な処理を行う必要があると思いますが、
必要最低限の設定は以上のような形になります。

Firebaseの設定を行ったり、ライブラリのリンクを設定したりも
JenkinsのJobにする事で比較的容易に行えるようになるので参考にして頂けると幸いです。