OGP

アプリ開発において CI が重要なのは Unity iOS プロジェクトでも同じだ. 個人開発として趣味の範囲で細々とやっている身であっても,むしろであるからこそ,CI による自動化で作業リソースや集中力を節約したい.

今回の検証では,iOS/Android アプリ開発でよく使われる CI のひとつである Bitrise を使って Unity iOS プロジェクトをビルドする方法を考える. 名もなき developer なので,Unity Personal ライセンスの有効化問題や Bitrise のビルド制限時間問題など「お金で解決できるはずの課題」が立ち塞がるが,強い気持ちと workaround でなんとかしていく.

概要

  • Ubuntu で Unity エディタを動かして Xcode プロジェクトを書き出す
  • Game CI が公開している Docker イメージを使うと便利
  • 書き出したプロジェクトを macOS の workflow でビルドする
  • プロジェクトの受け渡しは Bitrise の Artifacts で行う

環境

  • Unity 2020.1.1f1
  • Docker 20.10.0
  • Bitrise Stacks
    • Android & Docker, on Ubuntu 16.04 - LTS Stack
    • Xcode 12.3.x, on macOS 10.15.5 (Catalina)

背景

Unity iOS プロジェクトをビルドしたい場合,CI の選択肢としては大きく分けて次の3つの方針がある.

  • セルフホスト(以下 Jenkins を想定)
  • Unity Cloud Build
  • その他 iOS 向け CI サービス

もちろん,それぞれについて利点と課題がある.

Jenkins

Jenkins は,ある意味最も便利な選択肢と言える. Worker のマシンを直接セットアップできるので Unity はもちろん,必要なソフトウェアを予めインストールしておくことも簡単だし,いざとなればマシンに直接ログインしてデバッグをすることもできる. 一方で,セルフホストであることに伴うあれこれが,そのまま課題とも言える.

個人的にも以前は Jenkins を使って CI を構築していた.

Unity Cloud Build

Unity Cloud Build(以下 UCB)は,素直に一番簡単な選択肢だと思う. Unity Teams Advanced に$9/月を支払う必要があるが,Web の UI から必要最小限の設定で動かし始めることができる. 個人的にもかれこれ1年以上使っているが,CI のメンテがほぼ不要なのでとても楽になった.

しかし,UCB に課題がないわけでもない. UI が微妙で使い難かったり,自動ビルドが自動でオフになったり,証明書の更新が面倒だったりと気になる点はあるが,なかでも一番気になるのはビルドに時間がかかることだ. ビルド時間はプロジェクト構成に依存するので結果の数字だけ話しても大した意味はないが,UCB だと Git リポジトリに push してからビルド完了通知が届くまで60分程かかっている.

このスピードだと commit 毎にビルドするのは現実的ではないので main ブランチ更新があったタイミングでビルドするようにしている. 夜寝る前に feature ブランチをマージしておけば,翌朝には(運が良ければ)ビルドが配信されているはず…という感じで運用している.

Bitrise

その他 iOS 向け CI サービスは幾つかあるが,今回は Bitrise を対象とする. これは単にいつも使っているからという個人的な理由による.

利点としては,一言で言えば iOS 開発の現場で実際に使えるレベルの CI だということで,わざわざあれこれ挙げる必要はないだろう.Bitrise だと無料枠で macOS を使うことができる点も有難い.ただし,Unity iOS プロジェクトをビルドする場合に特有の問題として Unity エディタのライセンス有効化がある.

Unity ライセンス有効化問題

Unity エディタのライセンスを CLI から有効化する場合,Unity マニュアルの Command line arguments の定義に依れば -username-password を渡せば良さそうに見えるが,実はこの方法で有効化できるのは有償のライセンスのみで,無料の Unity Personal ライセンスには対応していない.

では Personal ライセンスは CLI で有効化できないのかというと,実はできないこともない. 手動のライセンス有効化(Manual Activation)という方法があって,次の手順で有効化できる.

  1. ライセンス有効化ファイル(.alf)を Unity エディタから書き出す
  2. Web ブラウザで Unity - Activation を開いてライセンスファイル(.ulf)を取得する
  3. ライセンスファイル(.ulf)を Unity エディタに渡してライセンスを有効化する

手順2において手動によるアカウント認証が必要(アカウント設定によっては 2FA も必要)になるが,その前後の手順は CLI で実行できる.

ただし,この方法ではライセンスファイルが worker のマシン ID と紐づくので,毎回別の仮想マシンインスタンスを立ち上げる CI では,ビルドの度にマシン ID が変わってしまうため,ライセンスファイルを再利用することができないという問題がある. この問題について「Unity CI ALF」で検索すると,これまでも色々と試行錯誤されてきたことがわかる. 主な方針としては仮想マシンの ID を固定するか,Puppeteer などを使ってライセンスファイル取得手順を自動化して毎回取得しなおすかになるが,macOS では前者の方針を取ることが難しい.

もし,Android など Linux でビルドできるプラットフォームが対象であれば,マシン ID を固定した Linux を使うことができる. では iOS を対象とする場合にはどうするか.

Ubuntu を使って Xcode プロジェクトを書き出す

というのが解決策となる(TL;DR).

Unity エディタの CLI では -buildTarget を指定することで任意の Build Target に対して実行ファイルやプロジェクトを書き出すことができる. そして,実は iOS 用の Xcode プロジェクト(.xcodeproj)を書き出すことは Ubuntu の上で実行している Unity エディタでも可能となっている.なんとっ!

もちろん,書き出した Xcode プロジェクトから iOS アプリをビルドするためには Xcode とそれを動かすための macOS が必要となるが,Xcode プロジェクトを書き出すまでであれば Ubuntu でも可能だ. つまり,ビルドの workflow を次のように分割すれば良い.

  1. Unity エディタから Xcode プロジェクトを書き出す(on Ubuntu)
  2. Xcode プロジェクトから .ipa をビルドする(on macOS)

Unity のライセンスが必要になるのは手順1のみなので,macOS で Unity ライセンスを有効化できない問題は問題にならない. Xcode プロジェクトが書き出せてしまえば,後は基本的には通常の iOS アプリのビルドと変わらないので,特に難しいところもない.

先行事例

有志による GameCI というプロジェクトがある. Unity のライセンス有効化やプロジェクトのビルドを行うスクリプトを公開している. Unity エディタをインストールした Ubuntu ベースの Dockerfile や Docker イメージも提供されている.

対象とする CI サービスについては GitHub Actions や Travis CI などの説明があるが,今回対象とする Bitrise については記述が無かった. また,Xcode プロジェクトから iOS アプリをビルドする方法についても具体的に説明が無かった. そこで,今回の検証では次の点について新たに考えて補足する.

  • Bitrise で Unity iOS プロジェクトをビルドする
  • Bitrise の複数 workflow 間で artifact を共有する
  • Unity iOS の Xcode プロジェクトから .ipa をビルドするときの注意点

以上で背景説明は終わり.

技術構成

Bitrise

今回の検証の中心. Stack は次の2つを使う.

  • Android & Docker, on Ubuntu 16.04 - LTS Stack
  • Xcode 12.3.x, on macOS 10.15.5 (Catalina)

Workflow と stack は 1:1 になるので,Xcode プロジェクト書き出し用と Xcode ビルド用で workflow を分けて,前段の workflow から後続の workflow をトリガーする形になる. Workflow を分割せずに,macOS の stack で Docker を動かして Ubuntu を起動する方法もありそうだが,次の2つの理由により Ubuntu stack を使うことにした.

  • デフォルトの macOS stack には,Docker がインストールされていない
  • ビルド時間が伸びると workflow の制限時間内にビルドが終わらない可能性がある

特に,制限時間については Free プランでは30分と短めなので,少し気を付ける必要がある.

Unity エディタ on Ubuntu

Unity エディタをインストールした Ubuntu マシンを用意する. 簡単のため GameCI プロジェクトが提供している Docker イメージを使う.

なお,Dockerfile も GitHub の GameCI で公開されているので,自前で Docker イメージを用意したり,そもそも Docker を使わずに Ubuntu を直接セットアップすることも可能.

Bitrise Artifacts

分割した workflows 間で Xcode プロジェクトを受け渡すために Bitrise Artifacts の仕組みを使う.

後続の workflow では,Bitrise API を使って artifacts をダウンロードする形で Xcode プロジェクトを受け取る.

ライセンス有効化ファイルの書き出し

まずは Unity エディタからライセンスファイルを書き出す. 基本的には1回実行すれば良いものなので,個別の Workflow として構築しておく. Docker を使うので Ubuntu の stack を使う.

GameCI 提供の Docker イメージが Docker Hub にあるので,そちらから pull して使う.Tags 一覧の ios モジュールを含むものから自分のプロジェクトに合ったバージョンを選んで,Env Vars に UNITY_DOCKER_TAG として設定しておく.

ビルドスクリプトは大した量でもないので Workflow に Script ステップを追加して書いてしまう. Unity を batch モードで起動して,書き出された .alf ファイルのパスを環境変数に書き込む. なお,unity-editor というのは GameCI の Docker イメージに設定されている bash スクリプトで,UnityEditor を batch モードで xvfb-run してくれるラッパーになっている. 詳しくは GitHub にある Dockerfile を見れば分かる.

後続の Deploy to Bitrise.io ステップを使って .alf ファイルを Bitrise に artifact としてアップロードする.

Workflow を実行して,実行結果のページから .alf ファイルを手動でダウンロードして,Unity - Activation を開き,Unity アカウントで認証してライセンスファイル(.ulf)を取得する. 取得したライセンスファイルを Code Signing の Generic File Storage に ID: UNITY_LICENSE_FILE としてアップロードしておく.

Xcode プロジェクトの書き出し

Unity エディタから Xcode プロジェクトを書き出す. こちらも Docker を使うので Ubuntu の stack を使う.

ライセンスファイルを復元して,Unity CLI に渡してエディタを有効化する.

肝心の Xcode プロジェクトの書き出しは,いつもどおり batch モードを使うだけで特に難しいところはない. 引数は最小限のものだけ渡しているが,いつもどおり Command line arguments から必要なものを使えば良い.

地味に重要なのが find -type l -exec しているところ. ビルドした Xcode プロジェクトが入ったディレクトリをまるっと artifact にアップロードしたいので,symlink を解決して実際のファイルに差し替えている. Symlink のままだと artifact の中に実際のファイルは含まれないため,後続の macOS を使った workflow でビルドするときにファイルを見つけられずリンカエラーになってしまう.

ということで,再び Deploy to Bitrise.io ステップを使って $BUILD_DIR をまるっとアップロードする. ディレクトリを Zip してアップロードするオプションがあるので true にしておくこと.

アップロードした artifact を後続の workflow に渡すために,ダウンロード URL を取得して workflow に引数として渡したい. Bitrise の API ドキュメントを参照すると GET リクエストで artifact の情報を JSON で取得できることが分かる. Artifact を指定するための slug は Deploy ステップが出力する $BITRISE_PERMANENT_DOWNLOAD_URL_MAP から雑に抜き出す.

実際の artifact ファイルをダウンロードできる URL は JSON の中に記録されているが,expiring_download_url という名前のとおり有効期限付きの URL になっているので,実際にファイルをダウンロードする直前に JSON を取得するのが良い.

GET リクエストを送る先の URL を ARTIFACT_URL に設定して,後続の workflow の入力として渡す. Workflow の実行は Trigger Bitrise workflow ステップを使えば良い. Workflow をトリガーするためのトークンは Bitrise アプリの Code タブから取得できる.

Xcode プロジェクトのビルド

最後に Xcode プロジェクトから iOS アプリをビルドする.

Artifact のダウンロード URL を取得して,Zip ファイルをダウンロードして,Zip を展開する. Bitrise API を叩くための API トークンが必要なので,Account Settings ページで生成して,Secrets の BITRISE_API_TOKEN に設定したものを読んでいる.

ここまでできれば,あとは通常の iOS アプリのビルドと変わらないので要点だけ補足する. 少し気を付ける必要があった点は次のとおり.

CocoaPods に時間がかかる問題

CocoaPods が必要なプロジェクト(Firebase SDK を使っているなど)の場合,Xcode プロジェクト書き出しを Ubuntu で行っているため,Unity の PostProcessBuild では pod install に失敗しているはずなので,macOS 側で xcodebuild する前に実行しておく. なお,Firebase SDK が生成する Podfile の source が GitHub を参照していて pod install にかなーり時間がかかるため,source の URL には CDN を使うように書き換えておくと時間を短縮できる.

Xcode アーカイブに時間がかかる問題

ひとまず Bitcode のオプションを切って短縮した. ただし,これだと App Store 版がビルドできなくなるので,それは今後の課題とする. なお,お金を払うという最強のソリューションが(以下略)

最後に Slack に通知して完了.

通知

まとめ・感想

Unity iOS プロジェクトを Bitrise でビルドすることができた.

  • Game CI プロジェクトが提供する Docker イメージを使った
  • Ubuntu を使うことで Unity Personal ライセンスでも CI の上で Unity を動かすことができた
  • iOS アプリのビルドは Workflow を分割して実装した
    • Ubuntu で Unity エディタから Xcode プロジェクトを書き出した
    • macOS で Xcode プロジェクトから iOS アプリをビルドした

肝心のビルド時間については,Bitrise だと30分強くらいで完了しているので,40-50% 程度は短縮できている. ただし,CI を構築できるまでに Unity Cloud Build の100倍ぐらい試行錯誤しているのは忘れることにする.

:doughnut:

References