この記事は Akatsuki Advent Calendar 2019 23日目の記事です。
前日は @ShaderError さんによる Unityでシェーダー描いてみたい でした。
アカツキ人事がハートドリブンに書く Advent Calendar 2019 もあるのでそちらもぜひ。
はじめに
以前に仕事でCircleCIを使ってUnityのAndroidとiOSビルドを行なっていたのでその事について書こうかなと思っていたのですが、最近GitHub Actions
という新機能がGitHubで公開されました。
これがいい感じにCircleCIでUnityビルドをしていた際に起きていた問題を解決していたので、試験的に試してみたまとめが本記事になります。
環境
GitHub Actions 12/23時点のワークフロー構文
Unity 2019.3.0f1
MacBook Pro (self-hostedで利用するマシン)
GitHub Actionsとは
GitHubによるCI/CDツールです。
詳しいことは GitHub Actions Documentation に書いてありますが、Jenkins
やCircleCI
などに替わる新しいツールとなるのか(個人的に)気になるサービスです。
GitHub Actions を導入する
前提:UnityProjectのリポジトリがGitHubに存在する状態であること
利用想定:self-hostedを使ってローカルにあるマシンをGitHub Actions
で利用する
前提の状態にするまでの解説は行いませんので、各自作業を行なってください。
GitHubのリポジトリの Setting -> Actions を選択して
Actions permissions
もEnable local and third party Actions for this repository
に変更してください
その後にSelf-hosted runners
のAdd runner
を選択
そうするとrunnerを追加するためのCLIのコマンドが表示されます。
各コマンドを順に実行してください。
実行した後にGitHubのページを再読み込みをするとSelf-hosted runners
にCLIコマンドを実行したPCが表示されていると思います。
これでGitHub Actions
でself-hosted
が使えるようになりました。
その後にGitHubのActionsタブにあるSimple workflow
を使って動作テストをしてみます。
runs-on
をself-hosted
にするのを忘れないでください。
変更が完了しコミットをすると、self-hosted
でbuild.yml
が実行されます。
なお、こちらに記載がありますが、ワークフローファイルは、リポジトリの.github/workflowsディレクトリに保存する必要があるのでご注意ください。
実行結果はActionタブから確認できます。
おそらく以下の画像のような結果になっていると思います。
これでローカルにあるself-hosted
に追加されたマシンから実行できるようになりました。
ひとまずこれで基本設定は完了になります。
Unityのビルドスクリプトを作成する
ここからは本格的にビルド処理を実装していきます。
まずはUnityでビルドスクリプトを書きます。
using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build.Reporting;
using System.Collections.Generic;
using UnityEditor.iOS.Xcode;
using UnityEditor.Callbacks;
public class MobileBuild
{
static string[] GetEnabledScenes()
{
return (
from scene in EditorBuildSettings.scenes
where scene.enabled
where !string.IsNullOrEmpty(scene.path)
select scene.path
).ToArray();
}
private static void BuildAndroid()
{
// Setting for Android
EditorPrefs.SetBool("NdkUseEmbedded", true);
EditorPrefs.SetBool("SdkUseEmbedded", true);
EditorPrefs.SetBool("JdkUseEmbedded", true);
EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
// Build
bool result = Build(BuildTarget.Android);
// Exit Editor
EditorApplication.Exit(result ? 0 : 1);
}
private static void BuildIOS()
{
// Setting for iOS
PlayerSettings.SetScriptingBackend(BuildTargetGroup.iOS, ScriptingImplementation.IL2CPP);
EditorUserBuildSettings.iOSBuildConfigType = iOSBuildType.Debug;
// Build
bool result = Build(BuildTarget.iOS);
// Exit Editor
EditorApplication.Exit(result ? 0 : 1);
}
private static bool Build(BuildTarget buildTarget)
{
// Get Env
string outputPath = GetEnvVar("OUTPUT_PATH"); // Output path
string bundleId = GetEnvVar("BUNDLE_ID"); // Bundle Identifier
string productName = GetEnvVar("PRODUCT_NAME"); // Product Name
string companyName = GetEnvVar("COMPANY_NAME"); // Company Name
outputPath = AddExpand(buildTarget, outputPath);
Debug.Log("[MobileBuild] Build OUTPUT_PATH :" + outputPath);
Debug.Log("[MobileBuild] Build BUILD_SCENES :" + String.Join("", GetEnabledScenes()));
// Player Settings
BuildOptions buildOptions;
buildOptions = BuildOptions.Development | BuildOptions.CompressWithLz4;
if (!string.IsNullOrEmpty(companyName)) { PlayerSettings.companyName = companyName; }
if (!string.IsNullOrEmpty(productName)) { PlayerSettings.productName = productName; }
if (!string.IsNullOrEmpty(bundleId)) { PlayerSettings.applicationIdentifier = bundleId; }
// Build
var report = BuildPipeline.BuildPlayer(GetEnabledScenes(), outputPath, buildTarget, buildOptions);
var summary = report.summary;
// Build Report
for (int i = 0; i < report.steps.Length; ++i)
{
var step = report.steps[i];
Debug.Log($"{step.name} Depth:{step.depth} Duration:{step.duration}");
for (int d = 0; d < step.messages.Length; ++d)
{
Debug.Log($"{step.messages[d].content}");
}
}
if (summary.result == BuildResult.Succeeded)
{
Debug.Log("<color=white>[MobileBuild] Build Success : " + outputPath + "</color>");
return true;
}
else
{
Debug.Assert(false, "[MobileBuild] Build Error : " + report.name);
return false;
}
}
private static string GetEnvVar(string pKey)
{
return Environment.GetEnvironmentVariable(pKey);
}
private static string AddExpand(BuildTarget buildTarget, string outputPath)
{
switch (buildTarget)
{
case BuildTarget.Android :
outputPath += ".apk";
break;
}
return outputPath;
}
}
大まかに書くとこのような感じになるかと思います。
ビルド後に実行する処理等は書いていませんので、必要に応じて追加してください。
ローカルで上記のスクリプトが動作するか以下のコマンドをCLIで確認してください。
# 適宜書き換えてください
export COMPANY_NAME=""
export PRODUCT_NAME=""
export BUNDLE_ID=""
export OUTPUT_PATH =""
/Applications/Unity/Hub/Editor/2019.3.0f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath ./ -executeMethod MobileBuild.BuildIOS -buildTarget iOS
/Applications/Unity/Hub/Editor/2019.3.0f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath ./ -executeMethod MobileBuild.BuildAndroid -buildTarget Android
CLIが実行できたのであればいよいよGitHub Actions
を使った話へ進みます
GitHub Actions で Unity のビルドを行う
Android Build
まずはAndroidのビルドを行います。
Unity2019からAndroidのNDKやJDKがインストール時に追加できるようになりました!
なのでNDKやJDKの設定は各自でお願いします(いい時代になりましたね)
早速build.yml
ファイルを書き換えましょう
name: ApplicationBuild
on: [push, pull_request]
env:
OUTPUT_PATH: ""
BUNDLE_ID: ""
PRODUCT_NAME: ""
COMPANY_NAME: ""
UNITY_VERSION: 2019.3.0f1
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v2
- name: Android Build
run: |
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \
-projectPath ./ -executeMethod MobileBuild.BuildAndroid -buildTarget Android
Androidビルドをするだけのシンプルな処理です。
環境変数は各自で適切に入力してください。
いくつかを説明を軽くしますと、
name
name:
はワークフローの名前になります。
この名前がリポジトリのアクションページにワークフローに表示されます。
on
on:
はワークフローをトリガーするGitHubイベントの名前です。
イベントの種類は多くありこちらにドキュメントとしてまとめてあります。
今回はpush
とpull_request
をトリガーにしています。
env
-
env
はワークフローの全てのジョブから利用できる環境変数を定義します。 -
jobs.<job_id>.env
は<job_id>
ジョブから利用できる環境変数を定義します。 -
jobs.<job_id>.steps.env
はステップから利用できる環境変数を定義します。
先ほどのbuild.yml
は 2. のenv
を使っています。
この後のiOSビルドでも使いますからね。
jobs.job_id.steps.uses
ジョブでステップの一部として実行されるアクションを選択します。
actions/checkout
はワークフローで使用できる標準アクションで、v2
を指定することでチェックアウトアクションのv2
を利用するという設定になります。(標準のアクションはこちらにまとまっています)
jobs.job_id.steps.run
オペレーティングシステムのシェルを使用してコマンドラインプログラムを実行します。
さらにshell
キーワードを使用すると、環境のOSのデフォルトシェルを上書きできます。
iOS Build
続いてiOSビルドです。
今回は時間がなくなったのと複雑になるので、ipaファイルをビルドするところまでは行わず、xcodeprojをビルドするところまで行います。
では、build.yml
を書き換えましょう。
name: ApplicationBuild
on: [push, pull_request]
env:
OUTPUT_PATH: ""
BUNDLE_ID: ""
PRODUCT_NAME: ""
COMPANY_NAME: ""
UNITY_VERSION: "2019.3.0f1"
jobs:
android-build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v2
with:
path: android
- name: Android Build
run: |
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \
-projectPath ./android -executeMethod MobileBuild.BuildAndroid -buildTarget Android
ios-build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v2
with:
path: ios
- name: iOS Build
run: |
/Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \
-projectPath ./ios -executeMethod MobileBuild.BuildIOS -buildTarget iOS
書き方が変わった箇所がありますね。
actions/checkout
にwith:
キーワードを使ってpath:
の設定を渡しています。
こうしている理由は、AndroidとiOSで利用するフォルダを分けてインポート時間やビルド時間の高速化するためです。
この状態でpushしてみましょう。
self-hosted
に登録したサーバーが動作してビルドが実行されるはずです。
AndroidとiOSのビルドが成功すると以下のような画面になります。
暗号化されたシークレットを利用する
実際に運用しようとなると、yml
に書きたくない情報を渡したいケースがあると思います。
その場合は GitHub のリポジトリの Setting -> Secret ページへいき Add a new secret
をクリック。
シークレットとは、暗号化された環境変数のことです。
Name
とValue
をそれぞれ入力します。
ここで設定した値をワークフローで利用するには
env:
SOME_PARAM: ${{ secrets.SOME_PARAM }}
のように記述することで可能です。
これでpassword
やTOKEN
の情報を渡すことができますね!
なお、シークレットの制限として
1. ワークフローで最大100のシークレットを持てる
2. シークレットの容量は最大64KB
の2点があります。
参考: 暗号化されたシークレットの作成と利用
終わりに
個人的にとてもいいツールだと感じました。
具体的には以下の点に可能性があると思います
1. マシンスペックをこちらで自由にカスタマイズできる(self-hostedの場合)
2. CI/CDがGitHubで完結する
3. Jenkinsから解放される(アセットバンドルをどうするのかという問題はありますが・・・)
他にもCircleCI(MacOS)と違ってUnityのインストールを毎回行う必要がなかったりと比較すると優れている点が多いなという印象です。
簡単な導入まででしたが、以上になります。
参考になれば幸いです。