Shellだけを使ってUnityをDockerでビルドする
はじめに
CIでテストやビルドすることが定番となって久しいですが、ネットにある参考記事は大抵がそのCIツール独自の書き方も込みのものが大半で、Dockerを使ってUnityをビルドする流れが実際よくわかん! となりました。
というわけで、手元で実装してみたところシェルスクリプトだけで実現できたので公開します。
Dockerfile
もdocker-compose
も使いません。
これで流れを掴んで、それぞれのCIツールの形式に当てはめていくとよいと思います。
ちなみにAndroidだけです。iOSはめんどくさすぎたので諦めました。
やってみた、というレベルなのでなにかあったら教えて下さい。
構成
このシェル内で使用するDocker関係は以下の通りです。
Image
name | desc |
---|---|
alpine/git | gitを使えるImage |
unityci/editor | unityビルド用のImage |
Volume
name | desc |
---|---|
license | Unityのライセンス認証用ファイルが入ったVolume |
tmp | alpine/gitでcloneしてきたプロジェクトをunityci/editorに渡すために作成する ビルドが終わったら削除 |
RootDir | gitプロジェクトのトップディレクトリの名前で作成される この中にビルドされたapkやログを入れる |
処理の流れはこうです。
準備
今回はわたしがCIビルドのテスト用に作成したこのリポジトリをcloneしてapkを作成していきます。
2019.4.24f1
Docker
定番のここから必要なUnityのイメージをpullしておいてください。
AndroidやiOSなど、プラットフォームによってそれぞれ必要なイメージが違います。
今回は2019.4.24のAndroid版をビルドするので、以下のコマンドでイメージをpullしておきます。
docker pull unityci/editor:ubuntu-2019.4.24f1-android-0.12.0
Unityビルド用のDockerイメージは1つ10GBくらい使うので、マシンのストレージとDockerのDisk Image Sizeをあらかじめ確保しておいてください。
また、メモリも最低4GBは使用できるようにしておかないと、たまにビルドに失敗します。
また、gitも使うのでgitが入ったalpine
のイメージもpullしておきます。
docker pull alpine/git
Unity
CLI
プロジェクト内にCLIを受け付けるエントリポイントを作っておきます。
このビルド用スクリプトだけでもいずれ1つ記事を書きたいところですが、今回はひとまず以下のように動くものと思ってください。
/opt/unity/Editor/Unity -projectPath "プロジェクトのパス" \
-batchmode -nographics -quit \
-executeMethod BuildExecutor.Build \
-target "ビルドするターゲットプラットフォーム" \
-out "apk出力先のファイルパス"
Unity License
Unityのライセンス認証ファイルをあらかじめ取得しておきます。
Unityのライセンス認証方法はCLIのオプションに-username -password -serial
を指定したりLicense Server立てたりとありますが、Dockerを使ってビルドする場合は静的ファイルでライセンス情報を保存しておくのが楽かなと思います。
ライセンス認証ファイルは以下の流れで作成します。
- 手元で.alfを作成
- Unity公式に.alfをアップロード
- .ulfをダウンロード
手元で.alfファイルを作成
docker run --rm \
-v license:/license \
-w /license \
unityci/editor:ubuntu-2019.4.24f1-android-0.12.0 \
/opt/unity/Editor/Unity \
-quit -batchmode -nographics -logFile \
-createManualActivationFile
Dockerのイメージはpullしたものを使ってください。Unityのバージョンさえ合っていれば対象プラットフォームが違うイメージにも使い回せるので大丈夫です。
このコマンドを叩くとlicense
というVolumeの中にUnity_v2019.4.24f1.alfが作成されます。
Volumeから取り出す必要があるので、filebrowserでブラウザから中をいじれるようにします。CIで引っこ抜いてもいいのですが、Unityの認証でもどうせブラウザを使うので、こいつ経由でやっちゃったほうが楽です。
docker run --rm -d \
--name fb \
-v license:/srv \
-p 80:80 \
filebrowser/filebrowser
コンテナが立ち上がったら上のURLをブラウザで開いて、admin/adminでログインするとlicense
の中身が見えます。
ここから.alfをダウンロードします。
Unity公式に.alfをアップロード
CIでビルドするために使うアカウントにログインした状態で上のページを開いて、先程ダウンロードしてきた.alfをアップロードします。
なんか聞かれますが適当に答えてると.ulfをダウンロードできるようになります。
.ulfをダウンロード
ダウンロードしてきた.ulfを先程のfilebrowserを使ってlicence
の中に入れます。
無事に.ulfの準備が終わったらfilebrowserを片付けます。
docker stop fb
実行
準備が……準備が長い……!
というわけでようやく実行です。
適当なディレクトリにこのシェルをダウンロードして、以下を実行してください。
引数の1つめがcloneするurl、2つめがブランチ名です。
./unity_build_shell.sh \
https://github.com/nekomimi-daimao/AutoBuildExample.git \
main
shell
- tmpのVolumeを作成
- alpine/gitのコンテナを起動
- gitをcloneしてブランチ名やハッシュ値などを取得
- ビルドに使う変数を定義
- ビルドに使うコマンドを定義
- unityci/editorのコンテナを起動してビルドを実行
shellの中身を見ながら説明を書いていこうと思います。
tmpのVolumeを作成
# 引数から必要なパラメータを取得
url=$1
checkout=$2
# ビルド用の一時ファイルのためにランダムな文字列を作成.範囲はa-zA-Z0-9
TMP=$(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)
# cloneしたファイルを置いておくtmpのVolumeを作成
docker volume create ${TMP}_VOL
alpine/gitのコンテナを起動
# 止められるように名前をつけて起動
# 先程作ったtmpのVolumeも渡してやる
# しばらくコンテナを起動しておきたいためentrypointをshで上書きする.そうしないと終了してしまう
docker run -itd --rm --name $TMP -v ${TMP}_VOL:/git/ --entrypoint "sh" alpine/git
gitをcloneしてブランチ名やハッシュ値などを取得
# clone
docker exec $TMP git clone $url
# .gitがあるパスがわからないとgitのコマンドが打てないので
# cloneしたディレクトリの直下にあるであろうパスを見つけてくる
dir_clone=$(docker exec $TMP ls)
dir_git="/git/${dir_clone}"
# checkout
docker exec $TMP git -C $dir_git checkout $checkout
# プロジェクトのルートディレクトリ.フルパスで出てくるので末尾だけ取る
dir_root=$(docker exec $TMP git -C $dir_git rev-parse --show-toplevel | xargs basename)
# ブランチ名.一応gitのコマンドで取り直す
branch=$(docker exec $TMP git -C $dir_git rev-parse --abbrev-ref HEAD)
# コミットハッシュ値.
hash=$(docker exec $TMP git -C $dir_git rev-parse --short HEAD)
echo
echo $dir_root
echo $branch
echo $hash
echo
# 使い終わったのでalpine/gitのコンテナを停止.
docker stop $TMP
ビルドに使う変数を定義
# ビルドに使うコンテナのイメージ.
UNITY_BUILD_IMAGE="unityci/editor:ubuntu-2019.4.24f1-android-0.12.0"
# licenseのVolumeに入っているライセンス認証ファイル名
LICENSE_FILE_NAME="Unity_v2019.x.ulf"
# ブランチ名_ハッシュ値.ディレクトリ名やファイル名に使う
build_state=${branch}_${hash}
ビルドに使うコマンドを定義
# gitでcloneしてきたファイルをコンテナのローカルにコピー
command_copy="cp -rp /git /build"
# ライセンス認証を行う
# このときライセンス認証のログを成果物のディレクトリに出力している
command_register="/opt/unity/Editor/Unity \
-batchmode -nographics -quit -silent-crashes \
-logFile /output/${build_state}/android/${build_state}_license.log \
-manualLicenseFile /license/${LICENSE_FILE_NAME}"
# UnityEditorでビルドを行うコマンド
# ビルド中のログを成果物のディレクトリに出力している
command_build="/opt/unity/Editor/Unity \
-batchmode -nographics -quit \
-logFile /output/${build_state}/android/${build_state}_build.log \
-projectPath /build/git/${dir_clone} \
-executeMethod BuildExecutor.Build"
# BuildExecutor.Buildに指定する引数.
# ここではプラットフォームとapkの出力先を指定している
command_arg="-target android \
-out /output/${build_state}/android/${build_state}.apk"
unityci/editorのコンテナを起動してビルドを実行
# UnityEditorが入っているunityci/editorのコンテナを起動
# licenseとgitでcloneしてきたtmpのVolumeはreadonlyを指定している
docker run -itd --rm --name ${TMP}_BUILD -w /build -v license:/license:ro -v ${TMP}_VOL:/git:ro -v $dir_root:/output $UNITY_BUILD_IMAGE
# プロジェクトをunityci/editorのローカルにコピー
docker exec ${TMP}_BUILD $command_copy
# ライセンス認証
docker exec ${TMP}_BUILD $command_register
# ビルドを実行
docker exec ${TMP}_BUILD $command_build $command_arg
# 済んだらコンテナを停止.--rmを指定しているので停止すると消える
docker stop ${TMP}_BUILD
# tmpのVolumeはもう誰も使っていないが
# 停止してすぐは使用中扱いになっているためちょっとだけ待つ
sleep 1s
# プロジェクトをcloneしてきたtmpのVolumeを削除
docker volume rm ${TMP}_VOL
結果
これでAutoBuildExample
というVolumeの中に成果物が格納されました。
もう一度filebrowserを使って中を見てみます。
docker run --rm -d \
--name fb \
-v srv:/srv \
-v AutoBuildExample:/srv/AutoBuildExample \
-p 80:80 \
filebrowser/filebrowser
コンテナが立ち上がったら上のURLをブラウザで開いて、admin/adminでログインするとapkとビルドログとライセンス認証ログが格納されていることがわかります。
ここで別のブランチcapsule
も同様にビルドします。
./unity_build_shell.sh \
https://github.com/nekomimi-daimao/AutoBuildExample.git \
capsule
ビルドが成功すると、こんな感じでブラウザから見ることができます。
同じLANにいるAndroidからは見えているので、Dockerを起動しているマシンのIPアドレスを調べればブラウザからapkをダウンロードしてインストールできます。
ブランチの差分を見ればわかると思いますが、apkをインストールして起動すると、mainの方はCubeが、capsuleの方はCapsuleが表示されます。
シェーダ壊れててピンクかもしれませんがまあそういうこともあるよね!
おまけ
apkですが、コンテナを立ち上げるたびに署名が変わって面倒です。
unityci/editorのAndroid向けイメージだと下のパスにapksignerが入っているので、これでビルド後に統一の署名をすれば楽と思います。
/opt/unity/Editor/Data/PlaybackEngines/AndroidPlayer/SDK/build-tools/28.0.3/apksigner
まとめ
長い道のりでした……。
これでもgitのclone処理がてきとうだったり、そもそもエラーを一切ハンドリングしていません。実環境の頂は遠い。
とはいえ、ひとまず流れと勘所はわかったと思うので、次はCIツールでUnityのビルドをやってみようと思います。今のところはconcourse。なんかオシャレなUIと用語とは裏腹に漢らしいスタイルがいいかなって思ってます。
あと、白状しますが、今回で一番時間かかったのはたぶんlicenseとlisenceの打ち間違いと間違ってる箇所を探すところでした。
おしまい。
つれづれ
cloneしてきたプロジェクトをそのままビルドするのでなく、いったんコピーしてからビルドしているのは、マルチプラットフォームならばそれぞれのunityci/editorのコンテナでビルドする必要があるから。
この作りだと.gitなどの不要なフォルダも渡してしまっているので、git archive
したzipを受け渡すのがあるべき姿か。
UPMの中身をパッケージ毎に取得するので、無駄な通信が発生しているのが気になるが、しかしそれこそがクリーンなビルド環境というものであり。悩ましいところ。
どうもビルドのログを見る限り、platformがstandaloneで起動した後にビルド対象のplatformに切り替えているような気がする。-buildTarget
などで事前に指定しておくべきか。
https://game.ci/docs/docker/docker-images
IL2CPP for Windows is not supported
とあるので、Hololens2を自動ビルドするのはだめそう。あったとしてもなんかいっぱいSDKをインストールした覚えがあるのでイメージのサイズが30GBとか行きそう。
--entrypoint "sh"
でentrypointが設定されたコンテナを起動したままにできることにたどり着くまでが長かった……。alpine/git
のusageは「ローカル環境にgitをインストールする代わりにaliasを設定して使う!!!!」とかいう過激派なこと書いてあって参考にならない。
よくよく考えなくても、ぜんぶ自前で作ろうとせずにGithubActionsのそれっぽいやつからそれっぽい箇所を抜き出してくればよかった。
参考
作った
修正
- 順番変えた
- まだlisenceって書いてたので直した
Discussion