概要
Unityのアプリはアセットサイズが膨らみがちです。そのため冪等性や保守コストはある程度諦めてJenkinsを使ってビルドする、ということも多かったです。
さて、そんな人が「最近Jenkinsのプラグインも保守大変だし、流行りのGithub Actionsっていうのに移行しようかな…でもクラウドのMacは高そうだし手元のMacをビルドマシンにしたいな…」
みたいなことを考えた時に知っておくと良さそうなことをまとめておきます。
無限にお金がある場合はGithub Hosting RunnerのLarge Runnerをぶん回した方が考えるコストが減るとは思います。
この投稿はKDDIテクノロジーアドベントカレンダーの12/8分の記事です。
他の記事はこちらです。
https://qiita.com/advent-calendar/2024/kddi-technology
先行事例調査
この3個が特にありがたかったです。
AWSを使ったクラウドUnityビルド環境の構築~GitHub Actions構築編~
https://synamon.hatenablog.com/entry/2022/05/02/180000
すごく詳細にまとまっていて、おそらく中規模以上のプロジェクトならこれを参考にするのがコスト戦略的に良い気がしました。
GitHub ActionsのセルフホストランナーでUnityビルドする
https://tech.framesynthesis.co.jp/github/actions-unity/
いつもお世話になっている korin さんのサイト。
物理ローカルマシンで動かすときのクリーンするかなどで参考にしました。
GitHub CI/CD実践ガイド
https://gihyo.jp/book/2024/978-4-297-14173-8
GithubActions自体の仕組みについて参考にしました。皆様も手元にある安心感のために買っておくといいと思います。
Github Actions自体のベストプラクティス
数週間前の僕が知っておきたかったことを書いておきます。
-
name: は絶対に指定する(後からの見やすさが違います)
-
shellはMacでもbashをデフォルトシェルにしておく
- そしてActions内のshellのbashはトップレベルで指定する(それにより、ログ出力が詳細になります)
# ワークフローのトップレベルでシェルオプションを指定する defaults: run: # bashでprofileを読む(Runner標準では読まない、self-hosted特有の処理 shell: bash -ieo pipefail {0}
-
複数のymlを作ることを躊躇しない(ios_build.ymlとandroid_build.ymlがあっても良いし build_check.ymlやaction_study.ymlなどがあっても良い)
-
ステップは細かく分ける(デバッグに便利です)
-
ifはstep単位で行う(よって、先に書いたステップを小分けするのが効いてくる)
- 例えばDEVビルド限定処理、などを書く時に使う
-
ステップ間の変数受渡はGITHUB_ENVを使う(グローバル変数扱いなので大きいActionsだともっと狭いスコープ用途のGITHUB_OUTPUTを使うのが推奨です)
step: - name: store_value run: echo "RESULT=hello" >> "${GITHUB_ENV}" #グローバルに変数書き出し - name: read_value run: echo "${RESULT}" # => hello
-
# コメントが書ける
ので、コメントは積極的に使う- コメントがないと後で読めなくなっちゃう!
-
VScodeなどでyamlを編集する時にyamlのチェックを入れてインデント崩れなどを避ける
-
困ったらshellのコマンドがそのまま使えるので、無理に既存アクションを組み合わせる為に消耗する必要は無い
- シェルのパスは${{ github.workspace }}が何もしてなければリポジトリルートのフルパス
- ${{ env.GITHUB_WORKSPACE }} ではないことに注意
- シェルのパスは${{ github.workspace }}が何もしてなければリポジトリルートのフルパス
-
ChatGPTなどが詳しい分野なので、機密情報を含まない一般的な範囲であれば活用する
- ただし、たまにshellの
if
に対応するfi
を付け忘れる、などのお茶目なところもある
- ただし、たまにshellの
GithubActionsの開発時の作法
開発中のアプリにCIを導入する時を例にします。
pushして試す、を繰り返すのでコミットが汚れがちなのですがSquash&Mergeでまとめることが出来ます。
小規模プロジェクトならこれが紛れがないんじゃないかな。という手順は以下です。
- (Self-Hosted-Runnerなら)クリーンな環境のマシンを用意する
- Actions開発用ブランチを作る(
feature/github_actions_dev
) - (一時的に)リポジトリのdefault branchを
feature/github_actions_dev
に変更する。なぜならActionsはdefault branchの内容で動くため - 開発中は
on: workflow_dispatch
を追加する(その方が手動実行できるため) - 手元でコードを書き換えて
feature/github_actions_dev
にpushして起動、を繰り返す - 無事動いたらdevelopなどのメインブランチにPRを出し、Squash&Mergeでコミットを綺麗にしつつマージする
- その後リポジトリのdefault branchを元に戻す
default branchではなく特定ブランチのActionsを起動させるテクニックはありますが、この方が明快で楽です。その際開発メンバーにはbranch変更の事を周知してください!
また、Runnerを途中で止めてSSH でログインし、シェル操作を試行錯誤すると開発効率が良いです。 https://qiita.com/shonansurvivors/items/cb8902acfe5c3a1b3ca0
Github Self-Hosted-Runner特有の注意事項
- 基本的にJenkinsと同じでRubyバージョンなどは端末内を参照する
- 冪等性の問題があるならActions内で明示的にインストールのactionを使う
- また、Self-Hosted-Runnerはビルドキャッシュなどが(明示的に消さない限り)残る
- 問題があるならビルド開始時に前回キャッシュを削除、などをshellで記述する
- 明示的なCleanにすると、UnityプロジェクトでLibrary以下が削除されて 死ぬほどビルドが遅くなる
- 一部のActionsはSelf-Hosted-Runnerだと動かないことがある(Xcodeのインストールアクションなど)
- runner.environmentの判定をifで行うことでself-hostedとそれ以外に両対応するように書けるが、メンテコストが地獄なのでself-hostedならworks on my machine の精神でやってしまうのもあり(Jenkinsの悪夢再びですが)
-
# runner.environment=>'self-hosted' または 'github-hosted' if [[ "${{ runner.environment }}" == "self-hosted" ]]; then echo "This job is running on a self-hosted runner." # Self-hosted ランナー専用のコマンドをここに追加 else echo "This job is *not* running on a self-hosted runner." # GitHub ホストのランナー専用のコマンドをここに追加 fi
Self-Hosted-Runnerのセットアップ
- 特定バージョンのXcodeをインストールしたり、特定SDKを入れたAndroidStudioをインストールしたりする
- 手動じゃなくてこれをGithub Actionsで実行できるようにしておいて、setup-self-hosted-runner.yml みたいなファイル名で保存しておくと、ビルドマシンを増やすときにやや楽
- しかし、それほどビルドマシンを増やす予定がないなら手でセットアップしても問題はない
署名情報(mobileprovisionやp12)の扱い
iOSアプリやAndroidアプリ、あるいはそれに限らず大事なファイル群やパスワード文字列群はどうするかという話。
パスワードなどは素直にGithubのActions内Secrets(あるいはRepository内Secrets)に保存すれば良いですが、p12ファイルやmobileprovisionファイルなどもgitignoreに登録して保護しましょう。
また、p12ファイルなどはBase64エンコードして文字列として上記パスワードと同じように保護して使うことができます。
- Base64エンコードしてSecretsに登録する
-
base64 someapp.mobileprovision > someapp.mobileprovision.base64
してMOBILEPROVISION_BASE64として登録する - Macなら
openssl base64 -in appkey.keystore -out appkey.keystore.base64
https://superuser.com/a/120815 - 実際に登録したSecretsを使うときの話
-
# ↑で保存して登録したMobileProvisionファイルをビルドマシン上で安全に復元する # note:この復元したファイルをビルド後に消しておくほうが安全ではある… - name: Setup Code Signing Files env: # ここで環境変数を定義しておく MOBILEPROVISION_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }} # 単純な文字列は以降 ${SOME_PASSWORD} で参照する SOME_PASSWORD: ${{ secrets.SOME_PASSWORD }} run: | # base64デコードしてリポジトリ上にsomeapp.mobileprovisionファイルを作る echo "$MOBILEPROVISION_BASE64" | base64 --decode > ./someapp.mobileprovision # モバイルプロビジョニングファイルをデバイスにインストール mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp ./someapp.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
-
エラー表示などの表示
echo "::error::Rubyがインストールされていません"
このように表示しておくと目立つので良い
このように書いておくとSummary上でも表示されるので、開発者に明示的なメッセージを伝えるときに使うと良い
echo "::notice title=現在のRubyバージョン::Current Ruby version: $(ruby -v)"
Unityプロジェクトのバージョンコード
ProjectSettings.assetにあるので、ここをUnityのビルド前に直接書き換える、なども手かも
AndroidBundleVersionCode:77
buildNumber:
Standalone: 0
iPhone:101
tvOS: 0
例えばこんな感じで…
# iOSのbuildNumberを強制的に書き換える
- name: Update iPhone build number in ProjectSettings.asset
run: |
FILE_PATH='./ProjectSettings/ProjectSettings.asset'
# ProjectSettings.asset内のbuildNumberの表記を探して行番号を取得
BUILD_NUMBER_LINE=$(awk '/buildNumber:/{print NR; exit}' $FILE_PATH)
echo "buildNumber: found on line $BUILD_NUMBER_LINE"
# そのbuildNumber表記を開始点としてiPhoneの表記を探す
IPHONE_LINE=$(awk -v start=$BUILD_NUMBER_LINE 'NR>=start && /iPhone:/{print NR; exit}' $FILE_PATH)
echo "iPhone: found on line $IPHONE_LINE"
# 今回置き換えるビルドナンバーの文字列を定義
NEW_BUILD_NUMBER=${{ github.event.inputs.iOSBuildNumber }}
awk -v line=$IPHONE_LINE -v new_number=$NEW_BUILD_NUMBER 'NR==line{sub(/[0-9]+$/, new_number)} {print}' $FILE_PATH > temp_file
echo "Before change:"
awk -v line=$IPHONE_LINE 'NR==line' $FILE_PATH
echo "After change:"
awk -v line=$IPHONE_LINE 'NR==line' temp_file
mv temp_file $FILE_PATH
Self-Hosted-Runner向けTips
AndroidSdkインストール
これはBuild Script内部で呼びます。往々にしてUnityのバージョンアップをサボっていると、TargetSDKだけが上がっていき、Android Studio内のSDKを参照したりカスタムgradleスクリプトを書くことに…
ミニマルなキャッシュ戦略
ある程度のアセットがあるUnityアプリの場合は、普通に(?)デフォルトの10GBのキャッシュを使い切るのでこういうのを使います。
Self-Hosted-Runnerを3-4台手元に用意しておいて、ファイルシステムとしてLAN内にぶら下げたNASをマウントしておく、などが良いと思います。
あるいはビルドマシンが1台なら物理的に同じ端末のSSD内にキーをつけてキャッシュ保存、 でも良いです。
少なくともSelf-Hosted-RunnerのUnityプロジェクトの場合は Github-Actionsの cache は使わない ことは知っておくといいと思います。
トラブルシューティング
ところでGithub ActionsのymlがLFSなんだけど・・・
えええ?そんなことあります?(あった)
影響範囲を最小にするために
- [attr] でnot-lfsを.gitattributesファイルの先頭に追加
- ymlをlfs指定しているすぐ下にworkflows以下のymlをlfs対象外に指定
.github/workflows/*.yml not-lfs
で乗り切りました。もしすでに書きかけの.github/workflows/build.yml ファイルがlfs管理だったら.github/workflows/super-build.yml とかにリネームしてシンプルにadd-pushするとlfsじゃなくなります。
.gitattributes 内で以下のように設定
# ファイル先頭でアトリビュートを定義
[attr]not-lfs -filter -diff -merge -text
# ここにfbxやpngなど様々なものが
# 定義されている、とおもってください
# ここに今回問題になったymlファイルのlfs設定があるので
*.yml filter=lfs diff=lfs merge=lfs -text
# こうして、明示的にworkflow以下はlfs化のホワイトリストに登録します
.github/workflows/*.yml not-lfs
Self-Hosted-RunnerのUnityが変
Error building player because build target was unsupported
という文字が出たら、諦めてUnityHubからマウスでぽちぽちして足りてないmoduleを入れましょう。
僕はiOSビルド環境がなぜか入ってなくてハマりました。
iOSのIL2CPPビルドで失敗する
Actual result: Xcode project build fails with “tundra: error: Failed to open file
みたいなログが出ることがあり、
- 新規プロジェクトでは問題ない
- 手元の開発PCでは問題ない
ということで悩んでいたら、IL2CPPのビルドキャッシュを消して再ビルドすると直るようです。
CIでは Library/Il2cppBuildCache/ を消すのを習慣にしても良いかもしれません。
SwitchPlatform前にコンパイルエラー出る
UnityをCIとかでバッチモードで動かす時は
-ignorecompilererrors
をつけると良い、ということがわかりました。
これつけるとStandaloneでコンパイルエラーが出てても -buildTarget Android とかのバッチモード実行がちゃんと動きます。
つまり、UnityのGameCIを使ってビルドしていて、 -ignorecompilererrors
を使ってるGithub Actionsのサンプルは信頼できそうですね!