もんりぃ is undefined.

育児ネタとか、技術ネタとか。

ごっこランドを支える技術 〜ビルド編〜

はじめに

この記事は、 Unity Advent Calendar 2018 の10日目の記事になります。
9日目の記事は gremito さんの AR上にShaderを動かしてみた という記事でした!

本記事は、株式会社キッズスターが開発・運営を行っているごっこランドというアプリの裏側を支える技術を少しずつ紹介する(予定の)連載 *1ごっこランドを支える技術」の ビルド編 になります。

ごっこランドは Unity 製モバイルアプリとして約5年に渡って運営されているのですが、月日を重ねるにつれて日々巨大化していくプロジェクト開発者を取り巻く環境・状況の変化への対応を行ってきました。
その結果として、様々な工夫が組み込まれたビルドシステムが構築されているので、本記事ではそのビルドシステムについて紹介をしていきたいと思います。

目次

  • 概要
  • 背景
  • 詳細

概要

もの凄くザックリ言うと、Slack にコマンドを打ち込んでしばらく待つと .ipa.apkDeployGate に配信される という仕組みです。

図で説明すると以下のような感じです。

image

上図をテキストでも解説すると以下のような感じです。

  1. Slack に jenkins build player <project_name> と発言する
  2. Slack Outgoing Webhook が実行される
  3. AWS API Gateway により発行された URL を叩く
  4. AWS Lambda にデプロイ済の Node.js アプリが実行されて Slack 上での発言を解析する
  5. 社内に配備してあるビルドサーバの Jenkins のパラメータ付きジョブを実行する
  6. Unity Editor を batchmode で起動し、 UnityEditor.BuildPipeline.BuildPlayer() を実行する
    • iOS の場合 PostprocessBuild 内で xcodebuild コマンドを実行し、アーカイブを作成する
    • Android の場合 PreprocessBuild 内で Keystore に関する設定を行う
  7. PostprocessBuild 内で作成されたアーカイブ ((iOS.ipaAndroid.apk)) を DeployGate にデプロイ
  8. DeployGate の Slack 連携設定により、Slack にデプロイ完了が通知される

詳細は次節以降で解説します。

Unity バージョン

2018/12/10 (Mon) 時点では Unity 2018.2.17f1 を用いています。

開発中は常に最新に上げ続けており、リリース内容が確定した時点で当該ブランチに於ける利用バージョンを最新バージョンでロックしビルドしています。

ビルドプラットフォーム

iOSAndroid 向け*2にビルドしています。

プロジェクトサイズ

$ du -sh Assets
5.1G    Assets

2018/12/10 (Mon) 時点でプロジェクトの総サイズは 5.1GB です。 Library/ ディレクトリも含めると 20GB を超えていました。

背景

上述の通りプロジェクトのサイズが割と大変なことになっているため、ごっこランドの開発では プロジェクト分割 というテクニックを用いています。

以下のスライドで簡単に纏めていますが、ざっくり言うと 個別の Unity プロジェクトとして開発したものを npm を使って1つのプロジェクトとして束ねる 仕組みを構築した感じになります。

そして、複数の開発プロジェクトが並行して走っている状態で、開発者一人一人の開発生産性を最大化する必要性が高まってきていました。
そのためには、個々の開発プロジェクトや束ねたプロジェクトを、開発者のマシンリソースを使わずにビルドできるようにする必要があります。
この課題を解決するために、専用のビルドサーバを配備*3し、開発者が任意のタイミングで実機ビルドを作成出来るようにしようと思い立ちました。*4

また、株式会社キッズスターでは、結構カジュアルにリモートワークが行われています。
そのため、VPN とかを使わずとも社内ネットワークにあるビルドサーバでビルドできるようにする必要性もあります。

更に、可能な限り API Token や認証鍵などの 秘匿すべき情報をリポジトリに載せたくない ので、その辺りにも気を遣う必要があります。

これらの課題を解決するために回りくどいシステムを構築した次第です。
次節から各項目について掘り下げていきます。

詳細

システム構成

改めて概要の図を掲示します。

image

Slack → AWS API GatewayAWS Lambda

いわゆる ChatOps の基本的な構成になるかと思います。

Slack の任意のチャンネルに対する発言などを任意の URL に対して POST してくれる Outgoing Webhook という仕組みを使います。
そんなに難しくないのでググれば簡単に設定できると思います。
AWS の権限管理の仕組みである IAM 辺りはハマるかもですが、基本はテンプレ通りにやれば問題ないかと思います。

Lambda に配置する関数としては、Slack の発言内容をパースして Jenkins のパラメータ付きビルド URL を構築する という要件を満たせば、どんな言語で書こうが問題ありません。*5
(今回のアドカレの趣旨からは外れてしまうので仔細は書きませんが、需要があればいつか別記事で掘り下げるかもしれません。)

AWS Lambda → Jenkins

組織によってはココが最大の障壁になると思います。
と、言うのも、AWS 側から社内ネットワークに向けた内向きのアクセスを許可する必要があるため、セキュリティが厳しい組織の場合ココで詰む可能性を秘めています。
その場合でも、外向きのアクセスは行けると思うので、Lambda からジョブパラメータを JSON か何かで S3 に Put して、Jenkins でソレをポーリングするとかもアリかもしれません。頑張ってください。

Jenkins → Unity Editor

Jenkins のジョブ設定で、シェルを叩くことができるので、 /Applications/Unity/Unity.app/Contents/MacOS/Unity を実行するようなシェル(後述)を書きます。

前提として、ビルド時のエントリポイントとなる public static なメソッドが必要になるので、何らかの形でプロジェクトに組み込んでおきましょう。
弊社では umm/simple_build: Provide BuildPlayer menu というライブラリを開発し利用しております。

Keystore 設定(Android のみ)

Android の APK を作成するにあたって、Keystore による署名を施す必要があります。
ビルド処理実行前に ProjectSettings を上書きしてあげる必要があるので、 UnityEditor.Build.IPreprocessBuildWithReport を実装したクラスの OnPreprocessBuild() メソッドにて UnityEditor.PlayerSettings.Android.(keystore|keyalias)(Name|Pass) を上書きします。
鍵の情報をリポジトリに載せるのは危ないので、環境変数などから貰うとヨサソウです。
弊社では、 umm/keystore_manager: Manage Keystore for Android というライブラリを用いています。

Build

UnityEditor.BuildPipeline.BuildPlayer() を実行します。

出力先は何でも良いと思いますが、弊社では simple_build 側の実装として <path_to_project>/Build/ の下にプラットフォーム毎・環境種別(開発か本番か)で分けて出力するようになっています。

Archive/Export (iOS のみ)

いくつか方法はありますが、PostprocessBuild として実装するのが筋が良いでしょう。
具体的には UnityEditor.Build.IPostprocessBuildWithReport という interface を実装したクラスの OnPostprocessBuild() メソッドにて /usr/bin/xcodebuild コマンドを実行する形になります。
Android の場合は直接 .apk ファイルが出力されるので、この手続きは不要です。*6

弊社では umm/xcode_archiver: Run xcodebuild on PostprocessBuild というライブラリを用いてアーカイブしております。

こちらを用いない場合でも、以下のようなコマンドを構成すれば Archive/Export が行えます。

Archive

Unity から出力された Xcode Project を .xcarchive としてアーカイブします。
Firebase などの CocoaPods を利用するような Native Plugin を用いている場合は -project の代わりに .xcworkspace へのパスを -workspace 引数に指定する必要があります。

$ /usr/bin/xcodebuild \
 -project "<path_to_build>/Unity-iPhone.xcodeproj" \
 -scheme "Unity-iPhone" \
 -archivePath "<path_to_build>/Unity-iPhone.xcarchive" \
 -sdk iphoneos \
 -configuration Release \
 -allowProvisioningUpdates \
 archive

Export

先ず、Export Option と呼ばれる .plist ファイルを作成します。*7

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>ad-hoc</string>
    <key>compileBitcode</key>
    <false/>
    <key>embedOnDemandResourcesAssetPacksInBundle</key>
    <false/>
</dict>
</plist>

続いて、作成した Export Option とあわせて xcodebuild コマンドを実行します。

$ /usr/bin/xcodebuild \
 -exportArchive \
 -archivePath "<path_to_build>/Unity-iPhone.xcarchive" \
 -exportPath "<path_to_build>/export" \
 -exportOptionsPlist "<path_to_build>/ad-hoc.plist" \
 -allowProvisioningUpdates

Deploy

検証用端末などにデプロイするために DeployGate というサービスを用いており、同サービスが提供するコマンドラインインタフェース*8を実行するコトでビルド成果物をデプロイします。

以下のようなコマンドを構成すれば OK です。

$ dg deploy <path_to_archive>

DeployGate の機能として、新しいビルドがデプロイされると Slack に通知する機能があるので、それを有効にしておくと便利です。

プロジェクト構成

「背景」の章でも述べたように、弊社では複数のプロジェクトとして開発したゲームコンテンツを一つのプロジェクトに束ねてアプリにしております。
そのテクニックや注意点などについて掘り下げて紹介します。

マージ

プロジェクトを束ねる際のツールとして npm を用いています。
個々のプロジェクトを npm のパッケージと見なして、本体のプロジェクト側の package.json に依存パッケージとして定義するコトでマージを行います。
その際、インストールされた個別プロジェクトの中身を Assets/Projects/ 以下に丸っとコピーするスクリプトを書いて postinstall*9 内で実行させています。

なお、実行速度の観点から、生の npm ではなく yarn を用いて実行しています。

ライブラリ管理

弊社では umm という仕組みを用いてライブラリ管理を行っています。
仔細は上記リポジトリをご参照いただくとして、概要としては各ライブラリを npm のパッケージとして取り扱えるようにすることで npm を用いた依存管理を行えるようにするものとなります。

現在の所、各ライブラリ npmjs へはパブリッシュしておらず*10GitHub にて公開しているモノを利用しています。

SortingLayer / Tag

複数プロジェクトをマージする関係上、SortingLayer や Tag などに代表される個々のプロジェクト設定として保存される情報は原則的にコピーできません。
これらの情報は ProjectSettings/TagManager.asset というファイルに保存されるのですが、全てのプロジェクトで設定が同じになるようにする必要があるため、無闇に増やせないなどの制約が生じています。

また、SortingLayer に関しては、表向き文字列ですが内部的には数値で管理しており、この値をプロジェクト間で統一することが難しかったため、独自のエディタ拡張*11を実装することで問題を回避しています。
特別な事情が無ければ SortingOrder だけで頑張るなどの工夫をした方が良いかも知れません。

Shader

個別のプロジェクトのスクリプトとシェーダを除くあらゆる Asset は基本的に AssetBundle として配信するため、設定に依ってはシェーダがアプリに含まれなくなることがあります。
そのため、都度 Always Included Shaders に含めるように設定するなどの作業が必要になる場合があります。

link.xml

Scripting Backend に IL2CPP を用いており、かつ AssetBundle を用いている場合、 Unity - Manual: Managed bytecode stripping with IL2CPP に記載があるように、一部のコードが削除されることがあります。
これを回避するために link.xml に情報を追記したり、 [Preserve] 属性を付けるなどの工夫が必要になることがあります。

Packages

2018年12月現在、 Unity Package Manager では GitHub などの git リポジトリからのパッケージ取得を完全にはサポートしていません。
詳細はブログを書いたのでそちらをご参照いただくとして、弊社では上述の通りライブラリの管理も npm を通して GitHub から取得する方法を採っているため、個別プロジェクト側で必要になったパッケージの依存解決を自動では行えません。
そのため、個別のプロジェクトで必要になったパッケージは、本体側のプロジェクトの manifest.json に手動で記載しないといけません。

この辺は Unity Package Manager が完全民主化されたあかつきには不要の悩みとなりそうです。

Assembly Definition Files

コンパイル時間の高速化とライブラリ間の循環依存を未然に防ぐために、Assembly Definition Files を積極的に導入しています。
各プロジェクトを個別の Assembly として切り出すため、 namespace などの重複は神経質にならなくても済むようになりました。*12

ビルドスクリプト

Jenkins が実行するビルドスクリプトの概要をご紹介します。

パラメータ

Jenkins のジョブとして受け付けるパラメータは以下のように設定しています。

Name Example Description
repository pretendland プロジェクトの名称
clone 先のルールを統一することで、リポジトリ名からパスを特定可能
branch develop/v4.10.0 ビルドするブランチ名
省略した場合は master をビルド
platform iOS ビルド対象のプラットフォーム
Switch Platform の効率を考えて、プラットフォーム毎に clone 先を分けている
editor_version 2018.2.17f1 起動する Unity Editor のバージョン
未指定の場合は ProjectSettings/ProjectVersion.txt の値を利用
development_build true 開発ビルドを作成する場合に真を指定

処理の流れ

  1. cd <path_to_project>
    • 先ず、パラメータとして渡された repositoryplatform から推定されるプロジェクトディレクトリに遷移
  2. git checkout <branch_name>
    • パラメータに指定されたブランチに checkout
  3. git lfs pull
    • サイズが大きいリポジトリの場合 Git LFS を用いているコトがあるため、実体を取得する
  4. yarn install
    • package.json, yarn.lock の情報に従い、パッケージを fetch, install
  5. /Applications/Unity/Unity.app/Contents/MacOS/Unity
    • simple_build のメソッドを実行
  6. git reset && git clean -fd
    • ビルド完了後、ビルドサーバ上のローカルリポジトリを掃除
  7. git checkout master
    • 元のブランチに戻って終了

Unity Editor 起動時引数

最低限、以下の引数が指定されていれば batchmode ビルドが可能です。

Argument Value Description
-quit ビルド完了時に Unity Editor のプロセスを終了させる
-batchmode Unity Editor の GUI は起動させない
-projectPath <path_to_project> プロジェクトのディレクト
Assets/ProjectSettings/ がある親ディレクトリを指定する
-executeMethod <method_name> 実行する public static なメソッド名を namespace を含む完全修飾名で指定
-logFile /dev/stdout Jenkins のログ管理に流すためにログの出力先を標準出力に設定する
-buildTarget [iOS|Android] ビルド対象のプラットフォーム
既にプロジェクトが Switch Platform 済であれば不要

そのほかの Unity Editor 起動時の引数は Unity - Manual: Command line arguments をご参照ください。

おわりに

ここまでお読みいただき、まことにありがとうございました!

もし貴方がオレオレビルドシステムを構築しようと思っており、本記事に構築の一助となるような情報があったのならば、心から嬉しく思います。

Unity Advent Calendar 2018 の11日目は copo さんの「インテリアマッピング(interior mapping)~その1~」です。

*1:本記事が第一回目なんですけどね。

*2:iOS SDKAndroid SDK ともに Latest でビルドしています。

*3:iOS のビルドを考慮し Mac mini を採用しています。

*4:勿論 Unity Cloud Build を用いる選択肢もあったのですが、柔軟性や後述のライブラリ管理の観点から断念しました。

*5:私は Node.js で書きました。

*6:Android Studio で開くためのプロジェクトを出力することも可能ですが、本記事では割愛します。

*7:この例では Bitcode や OnDemand Resources などを無効にしていますが、プロジェクトにあわせて変更してください。

*8:Ruby の gem として提供されています。

*9:記事執筆時点でレイアウトが崩れていましたが、頑張れば読める…かな?

*10:中身は Node.js じゃないので Ban されても嫌だな、というコトで。

*11:現在の所、弊社 Organizations の private リポジトリで管理しています。

*12:とはいえ、可読性などの観点から namespace の設定ルールは割と厳格にしていますが。