Shellだけを使ってUnityをDockerでビルドする

11 min read読了の目安(約10000字

はじめに

CIでテストやビルドすることが定番となって久しいですが、ネットにある参考記事は大抵がそのCIツール独自の書き方も込みのものが大半で、Dockerを使ってUnityをビルドする流れが実際よくわかん! となりました。
というわけで、手元で実装してみたところシェルスクリプトだけで実現できたので公開します。
Dockerfiledocker-composeも使いません。
これで流れを掴んで、それぞれのCIツールの形式に当てはめていくとよいと思います。
ちなみにAndroidだけです。iOSはめんどくさすぎたので諦めました。

やってみた、というレベルなのでなにかあったら教えて下さい。

https://gist.github.com/nekomimi-daimao/9e4a8ff0e68818574f971a7ff86a536a

構成

このシェル内で使用する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

https://github.com/nekomimi-daimao/AutoBuildExample

Docker

https://game.ci/docs/docker/versions

定番のここから必要なUnityのイメージをpullしておいてください。
AndroidやiOSなど、プラットフォームによってそれぞれ必要なイメージが違います。

今回は2019.4.24のAndroid版をビルドするので、以下のコマンドでイメージをpullしておきます。

pull image
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しておきます。

pull git
docker pull alpine/git

Unity

CLI

https://gist.github.com/nekomimi-daimao/e9fe411cc5ad14f8700e7adb06a883a1

プロジェクト内にCLIを受け付けるエントリポイントを作っておきます。
このビルド用スクリプトだけでもいずれ1つ記事を書きたいところですが、今回はひとまず以下のように動くものと思ってください。

BuildExecutor
/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を使ってビルドする場合は静的ファイルでライセンス情報を保存しておくのが楽かなと思います。

ライセンス認証ファイルは以下の流れで作成します。

  1. 手元で.alfを作成
  2. Unity公式に.alfをアップロード
  3. .ulfをダウンロード

手元で.alfファイルを作成

createManualActivationFile
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の認証でもどうせブラウザを使うので、こいつ経由でやっちゃったほうが楽です。

filebrowser
docker run --rm -d \
--name fb \
-v license:/srv \
-p 80:80 \
filebrowser/filebrowser

http://localhost:80

コンテナが立ち上がったら上のURLをブラウザで開いて、admin/adminでログインするとlicenseの中身が見えます。
ここから.alfをダウンロードします。

IDとパスワードがデフォルトなのでブラウザから怒られることもありますが気にしないでください

Unity公式に.alfをアップロード

https://license.unity3d.com/manual

CIでビルドするために使うアカウントにログインした状態で上のページを開いて、先程ダウンロードしてきた.alfをアップロードします。
なんか聞かれますが適当に答えてると.ulfをダウンロードできるようになります。

.ulfをダウンロード

ダウンロードしてきた.ulfを先程のfilebrowserを使ってlicenceの中に入れます。

.ulfはライセンス認証を保持しているファイルなので、取り扱いには気をつけてください

無事に.ulfの準備が終わったらfilebrowserを片付けます。

stop fb
docker stop fb

実行

準備が……準備が長い……!
というわけでようやく実行です。

https://gist.github.com/nekomimi-daimao/9e4a8ff0e68818574f971a7ff86a536a

適当なディレクトリにこのシェルをダウンロードして、以下を実行してください。
引数の1つめがcloneするurl、2つめがブランチ名です。

do
./unity_build_shell.sh \
https://github.com/nekomimi-daimao/AutoBuildExample.git \
main

shell

  1. tmpのVolumeを作成
  2. alpine/gitのコンテナを起動
  3. gitをcloneしてブランチ名やハッシュ値などを取得
  4. ビルドに使う変数を定義
  5. ビルドに使うコマンドを定義
  6. 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を使って中を見てみます。

filebrowser_again
docker run --rm -d \
--name fb \
-v srv:/srv \
-v AutoBuildExample:/srv/AutoBuildExample \
-p 80:80 \
filebrowser/filebrowser

http://localhost:80

コンテナが立ち上がったら上のURLをブラウザで開いて、admin/adminでログインするとapkとビルドログとライセンス認証ログが格納されていることがわかります。

ここで別のブランチcapsuleも同様にビルドします。

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と用語とは裏腹に漢らしいスタイルがいいかなって思ってます。

https://concourse-ci.org/

あと、白状しますが、今回で一番時間かかったのはたぶん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のそれっぽいやつからそれっぽい箇所を抜き出してくればよかった。

参考

https://www.codit.work/codes/wn2rlkmpr4spwne75n82/
https://knowledge.sakura.ad.jp/tags/docker/
https://tsunokawa.hatenablog.com/entry/2019/07/17/100000
https://www.atmarkit.co.jp/ait/articles/1810/07/news001.html
https://docs.unity3d.com/ja/2019.4/Manual/CommandLineArguments.html
http://neue.cc/2019/04/08_574.html
https://tech.mktime.com/entry/242
https://qiita.com/dalance/items/b04c288d8dde06ce2b59
https://www.atmarkit.co.jp/ait/articles/1801/25/news025.html
https://blog.dalt.me/736
https://gotohayato.com/content/85/
https://qiita.com/kitsuyui/items/35c15b2b46a30ed82d47
https://qiita.com/Vit-Symty/items/5be5326c9db9de755184

作った

https://gist.github.com/nekomimi-daimao/9e4a8ff0e68818574f971a7ff86a536a
https://gist.github.com/nekomimi-daimao/e9fe411cc5ad14f8700e7adb06a883a1
https://github.com/nekomimi-daimao/AutoBuildExample

修正

  • 順番変えた
  • まだlisenceって書いてたので直した