yukiarrr開発月誌

日誌になれなかった月誌(怠け)

「GitHub ActionsでUnityビルドしてipa吐き出すActions作ろ」と思ったら大苦戦した話

後日、どこかで発表できるといいなぁと思っています。

目次

はじめに

GitHub ActionsのActionsとは、一連の処理をパッケージングして簡単に使えるようにした、CircleCIでいうところのOrbsみたいなものです。
例えば、リポジトリをcheckoutしたい場合、以下の記述だけで可能です。

jobs:
  unity-build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v2 # これだけ

超簡単ですね。

このActionsは誰でも作成&公開が可能で、実際にいくつかパッケージングされたものが配布されています。
今回は、「Unityビルドしてipa吐き出す」ということで、UnityビルドとiOSビルドの2つのActionsを作りますが、このうちUnity側はすでに存在していました。

もともとはそのActionsに乗っかる気満々だったのですが、実際に使ってみるとLinux上限定で動くActionsだったため、iOSビルドに繋げづらく...
とはいえ、Unityビルドはよく使うので、今回Actionsを作ることに決めました。

UnityビルドのActions作成(断念)

Dockerを使ってビルドを試みる

実際Actionsを作っていくにあたって真っ先に考えるべきことは、Actions自体の実装方法です。
この実装方法は公式で書かれていて、主に以下の2つになります。

  • Dockerでの実装
  • JavaScriptでの実装

このうち、Mac限定のiOSビルドはさておき、Unity側は他のCI系サービスでDockerを使うことが多く、GitHub ActionsでもDockerを使おうと考えていたので、その流れで「Dockerでの実装」を選択しようとしました。
使用するイメージは皆さんおなじみのgableroux/unity3dです。

ただ、このDocker実装のActionsには一つ問題点がありました。
それは、Linuxでしか動かない点です。

GitHub Actions上のOSで、DockerがインストールされているのはLinuxのみなので、当然といえば当然なのですが、Mac上で「Dockerでの実装」のActionsが動かないとなると、Unityビルドと同じJobでiOSビルドが実行できません。

つまり、この時点で「JavaScriptでの実装」が決定したわけです。
(でもDockerは使いたいので、JavaScriptから無理やりdockerコマンド叩くやり方)

ただ、依然としてMacにDockerが入っていない事実は変わりません。

そこで自分が考えたのは、Macにbrew等を使ってDockerをインストールする戦法です。

補足
ここから先、shellコマンドばかり出てきますが、全てJavaScriptのchild_processを使い、直接呼び出しているものとなります。

childProcess.spawnSync("/bin/bash", [`${__dirname}/何かのスクリプト.sh`], {
  stdio: [process.stdin, process.stdout, process.stderr],
  encoding: "utf-8"
});

(なお、@actions/execを使えばもっと楽にできますが、最初はそれを知りませんでした...)

brewを使ってインストール

brew install docker
brew cask install docker

ググったらすぐに出てくるインストール方法をまずは手始めに試してみました。
結果としては、うまくいきました。

「お、これは簡単にいきそうだな」と思った矢先でした。
実際にUnityイメージを使ってdocker runすると、下記のエラーが発生しました。

docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

見たところ、Dockerがそもそも起動していないようです。
そういえば、Dockerを立ち上げていないと思い、

open -a Docker

上記のコマンドを実行しましたが、結果は変わらず。
ただ、これに関しては薄々予想通りで、Dockerが立ち上がるまで待ってやる必要があると考え、

while ! docker system info &>/dev/null; do
  sleep 10
done

こんな感じのコマンドで、Dockerが動き出すまで待機するようにしました。
ところが...

起動しないDockerとライセンス問題

全くDockerが起動しませんでした。
他にもいくつか試しましたが変わらず。

ここら辺で、他にも同じことを試している人間がいるに違いないと思い、GitHub ActionsでDockerをインストールする手順をググってみると...
なぜ、MacにDockerがインストールされていないか質問している記事があり、そこにはこう書かれていました。

As mentioned in the linked ticket, docker should be not supported on hosted macOS.

Unfortuately the docker community licensing is such that we are not able to install the docker for mac on our hosted runners. We have had some discussions with Docker about doing this but they have held firm on their request that using Docker on a service requires an enterprise license and docker enterprise is not at all supported on macOS.

For Windows Microsoft holds the license for Docker Enterprise on Windows so we do not have any issues there and for Linux we use a specific build of Docker for Linux that is built and maintained by Azure.

引用: https://github.community/t5/GitHub-Actions/Why-is-Docker-not-installed-on-macOS/td-p/39364

要約すると、「ライセンス的に無理」です。
つまり、仮にMacにDockerをインストールできたところで、そもそもライセンス的にNGの可能性があります。

Actionsを公開する以上、こういった問題は避けたいため、この時はMac上でUnityビルドすることは諦めました。

Linux上のみでビルドする

Mac上でDockerを使いUnityビルドすることが無理とわかったため、諦めてLinuxのみにすることにしました。
(この時点で「Linux限定なら、すでに公開されているActionsでいいんじゃ」となりますが、そういった華麗なツッコミは一旦スルーで)

ちなみに、iOSビルドに関しては別のJobで実行することにしました。
ファイルの受け渡しにはArtifactsを使用しています。
(後に詳細がありますが、これがまた地雷)

「Dockerでの実装」をしよう!(失敗)

Linux限定なら、わざわざJavaScriptからdockerコマンドを叩かずとも、「Dockerでの実装」に切り替えればいいと思い、実装を進めました。

ただ、ここで一つ問題が。
DockerfileのFROMの部分がどうしてもいじれないのです。

具体的にやりたいこととしては、

ARG  CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD  /code/run-app

FROM extras:${CODE_VERSION}
CMD  /code/run-extras

参考: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact

このように、FROMに変数を使い、バージョンを可変させようと考えました。

しかし...

このためには、dockerコマンドでいうところの--build-argでバージョンを渡す必要があります。
GitHub Actionsの「Dockerでの実装」の構文の一つに、argsというのがあり、これでいけるか!?と期待しますが、残念ながらこれは実行時にENTRYPOINTに渡されるものなので、使えません。

他に見た限りも、このバージョンを可変させるすべはなさそうで、この時点で「Dockerでの実装」を断念せざるをえませんでした。

やっぱり「JavaScriptでの実装」か、そう思ったが...

この時点で、Linux限定なら既存のActionsであるUnity - Builderでいいことに気がつきました。

今までの時間...

iOSビルドのActions作成

無事、Unityビルドができるようになりました。
次は、出力されたプロジェクトをXcodeを使ってipa化する作業です。
これができれば、一通りパイプラインが構築されたことになります。

Job間のデータ受け渡し

UnityのビルドはLinux上で行われますが、ipaを作成するにはXcodeが必要なため、Mac上に切り替える必要があります。
この際に公式で書かれているのがArtifactsを使った受け渡し方法です。

具体的には、LinuxのJobでUnityビルド後に生成されたプロジェクトをArtifactsとしてアップロードし、MacのJobでそれをダウンロードし、Xcodeでビルドするといったやり方です。

正直、Unityのビルド結果はGB単位になったりするので、極力避けたい方法ですが、とはいえ他に方法もないので、これで実装を進めました。

容量上限と変わらない使用容量

当然ですが、何度かテストしているとArtifactsが容量上限に引っかかってしまいました。
もちろん、お金を投入すれば上限をあげられるのですが、さすがにテスト段階で投入したくはありません。

そこで、一旦上限を上げるため、古いArtifactsをUIをぽちぽちしながら消して回ったのですが...
なぜか、所属する組織 > Settings > Billing > GitHub Packagesに表示される、使用中の容量が変わりません。

これでは毎回上限に引っかかってJobを走らせることができません。
しかし、その日は遅かったため、諦めて寝たところ。
次の日には、使用中の容量が下がっていました。

どうやら、Artifactsを消しても反映されるのに時間がかかるようです。
とはいえ毎回待たされるのもあれなので、先んじてワークフロー終了時にArtifactsを削除しようと考えました。

消せないArtifacts

Artifacts削除をサボっていたことによる問題が発生したため、ワークフロー終了時にArtifacts削除を試みたのですが...

こちらの投稿によると、どうやら削除するには削除APIを叩くしかすべがないようです(actions/delete-artifactsのようなものはない)。
また、削除APIはワークフローそのものが完了した時にしか呼べないため、終了時に削除ということができないようでした。

つまり、cronで定期的に削除APIを叩くようにするしかありません。

理想としては投稿内でもある通り、

    - name: Upload build
      uses: actions/upload-artifact@master
      with:
        name: installer
        path: installer.exe
        retention-days: 5 # 実際にはできません!!

のように有効期限を持たせることができればいいのですが、それは現状のGitHub Actionsでは難しく...

そのため、自分は一旦S3にあげる方向で進めるようにしました。
ちなみに、cronで定期的に削除APIを叩く方法を実際にActionsとして公開されている方もいるため、そちらもご参考ください。

iOSビルドのActionsを作成する

iOSビルド用に、yukiarrr/ios-build-actionを作成しました。

github.com

会社や受注だと、開発者用のログイン情報や権限はもらえず、p12やmobileprovisionだけを渡されることも少なくないので、それに対応したActionsになっています。

具体的には、

- uses: yukiarrr/ios-build-action@v0.5.0
  with:
    project-path: Output/Unity-iPhone.xcodeproj
    p12-base64: ${{ secrets.P12_BASE64 }}
    mobileprovision-base64: ${{ secrets.MOBILEPROVISION_BASE64 }}
    code-signing-identity: ${{ secrets.CODE_SIGNING_IDENTITY }}
    team-id: ${{ secrets.TEAM_ID }}

こんな感じで書けば、iOSビルドしてくれます。
(デフォルトではoutput.ipaというファイルを出力する)

ここまで来るのに色々と苦戦していますが、iOSのActions作成自体は、GitHub ActionsのMac上に最初からfastlaneが組み込まれていることもあり、すんなりと作成することができました。

こけるiOSビルド

さて、これでいざiOSビルドを試してみると、

.../MapFileParser.sh: Permission denied
Command PhaseScriptExecution failed with a nonzero exit code

見るに、権限周りで失敗しているようです。
確かに実行権限がないので、スクリプトに対してchmod +xして回ったところ、今度は別のエラーが発生してしまいました。

そのエラーはどちらかというとプロジェクト固有のもので、どうやらUnityがインストールされている必要がありそうでした。
ただ、正直回避可能なエラーで、それほど大きな問題ではなかったのですが、このタイミングでUnityビルドをLinuxで行うという方法を取りやめました。
というのも、Jobを分けたことによるトラブルにより、それ専用の対応が結構面倒になってきたためです(特にArtifacts)。

この結果、最終的な形としてはUnityビルドもiOSビルドも同じMac上で行うことになりました。

(ただ、全てMac上でやるとなると使用可能時間をかなり食うので、一概に正解とはいえません)

UnityビルドのActions作成(再開)

u3dでインストール

Macでdockerコマンドが使えないとなると、直接Unityをインストールするほかありません。
以前、Unity HubのCLI版がPreviewで出たと聞いた覚えがあったので、検索して試そうと思ったのですが、2020年2月時点ではドキュメントはなく、そもそもの使い方がまったく不明だったため、今回は見送りしました。

ただ、幸いネットにはUnityのCLIインストーラーを用意してくださっている方がいます。
それらは複数見受けられますが、今回はu3dを使用しています。

u3dでの注意点

u3dを使用するにあたって、いくつか注意点があります。
それは、u3d自体のインストールとUnityのインストールは、管理者権限で実行する必要がある点です。

具体的には、

sudo gem install u3d
sudo u3d install $UNITY_VERSION -p Unity

としてください。

u3dでのライセンス認証

Dockerの際にはライセンスファイルを生成する手段が取れたかと思いますが、u3dなどで実際にビルドマシンにUnityをインストールする手段をとる場合、ライセンス認証はライセンスファイルを生成したマシンでしか認証できないため、クラウド上のMacを使用するGitHub Actionsでは同じ手段が取れません。

そのため、メールアドレスとパスワード、シリアルコードを使用した認証方式を使用しました。
なお、Personalでのシリアルコードの取得方法は、以下の記事をご覧ください(裏技っぽい)。
参考: UnityのライセンスでPersonal・Proなどプラン問わずシリアルコードを取得する

また、ライセンスの返却ですが、これはGitHub Actionsの終了時に呼ばれるpostで実行しています。

runs:
  using: "node12"
  main: "build.js"
  post: "returnlicense.js"

実はこのpost、GitHub Actionsのドキュメントには乗っておらず、隠しコマンド的存在になっています。
(actions/checkoutなどを見ればpostを使用していることがわかる)

成果物

これらをまとめたActionsが、yukiarrr/unity-build-actionです。
どの仮想環境上でも使用可能なことが特徴です。

github.com

Unityをビルドするためのコードは、以下のようになりました。
(ipa生成は除く)

- uses: yukiarrr/unity-build-action@v0.5.0
  with:
    unity-version: 2018.4.12f1
    unity-username: ${{ secrets.UNITY_USERNAME }}
    unity-password: ${{ secrets.UNITY_PASSWORD }}
    unity-serial: ${{ secrets.UNITY_SERIAL }}
    build-target: iOS

まとめ

結果的に、Unityをビルドしてipaを吐き出すまでに必要なコードはご覧の通りです。

- uses: yukiarrr/unity-build-action@v0.5.0
  with:
    unity-version: 2018.4.12f1
    unity-username: ${{ secrets.UNITY_USERNAME }}
    unity-password: ${{ secrets.UNITY_PASSWORD }}
    unity-serial: ${{ secrets.UNITY_SERIAL }}
    build-target: iOS
- uses: yukiarrr/ios-build-action@v0.5.0
  with:
    project-path: Output/Unity-iPhone.xcodeproj
    p12-base64: ${{ secrets.P12_BASE64 }}
    mobileprovision-base64: ${{ secrets.MOBILEPROVISION_BASE64 }}
    code-signing-identity: ${{ secrets.CODE_SIGNING_IDENTITY }}
    team-id: ${{ secrets.TEAM_ID }}

公開したOSSは以下の2つです。

github.com

github.com

この他にDeployGateへのアップロードや、Slackへの通知も一緒にやっておくと良いかと思います。

ただ、Mac上でやることにより、使用可能時間の消費が尋常ではないので、料金面を考慮するなら、依然としてself-hostedなど自前の環境がお勧めになってしまいます。

やはり環境固定のビルドは辛いことが多いですね...

では皆さま、体にはお気を付けて。
お読みいただき、ありがとうございました。

UnityのライセンスでPersonal・Proなどプラン問わずシリアルコードを取得する

すごく裏技みたいです。

目次

はじめに

UnityをCLIで実行する際、手始めに認証する必要がありますが、その認証方法として、

  • ユーザー名、パスワード、シリアルコードでの認証
  • ライセンスファイルでの認証

の2つがあります。(Unity HubなどGUI経由だと、知らず知らずのうちにライセンスファイルが生成されています)

このうち、ライセンスファイル形式はファイルを作成したマシン上でしか認証することができず(コンテナ上なら共有できる)、CI/CDで適当なVMが割り当てられる場合だと認証が通らなかったりするので注意が必要です。

「なら、作成もそのVMでやってやればいいんじゃ」となりますが、この生成にはブラウザを経由する必要があり、無理をすればできないこともありませんが、ブラウザ側のUIが変わる可能性もあるので、お勧めできません。

ので、適当なVMが割り当てられるCI/CDの場合、ユーザー名、パスワード、シリアルコードでの認証を選択することになります。
そこで問題になるのが、Personalライセンスでのシリアルコードの取得です。

PersonalライセンスはProライセンスのようにメールでシリアルコードが送られてきたり、ブラウザで確認することもできないので、取得する術がありません。

ただ、Personalライセンスでも存在することだけは分かっていたので、どうにかこうにか弄って見つけた裏技ような取得方法を共有します。
(Proライセンスでも使える方法です)

取得手順

ライセンスファイルを取得する

シリアルコードを取得するために、まずはライセンスファイルを取得する必要があります。

そのため、まずはライセンス認証そのものをする必要がありますが、こちらも2種類の方法があります。

このうち、前者の場合は、最後の手順でダウンロードするulfファイルがライセンスファイルです。

後者の場合、すでにライセンスファイルが生成されています。以下のディレクトリを漁ってみてください。

OS パス
Mac /Library/Application Support/Unity/xxx.ulf
Windows C:\ProgramData\Unity\xxx.ulf

ライセンスファイルからDeveloperDataを探す

ライセンスファイルは拡張子がulfでxml形式となっています。
この中から、DeveloperDataを探してください。

<DeveloperData Value="WFgtWFhYWC1YWFhYLVhYWFgtWFhYWC1YWFhYCg=="/>

このような記述が見つかると思います。
(このすぐ下にSerialMaskedというそれっぽい値があると思いますが、これはマスクされているので無視してください)

DeveloperDataをデコードすると…

このDeveloperDataの値はbase64エンコードされているので、デコードします。

$ echo "WFgtWFhYWC1YWFhYLVhYWFgtWFhYWC1YWFhYCg==" | base64 --decode
XX-XXXX-XXXX-XXXX-XXXX-XXXX

こちらのコマンドを実行するか、Base64のデコード - オンラインBase64のデコーダなどのサイトでデコードしてください。

上記は適当な文字列にしていますが、実際の環境で試してみると、ちゃんとしたシリアルコードが取得できているかと思います🎉
(先ほどのSerialMaskedの、マスクされてない値が取得できているはず)

まとめ

お疲れ様でした。
これでシリアルコードの取得は完了です。

この方法は、前述通りPersonal・Proなどプラン問わず適応可能なので、シリアルコードが必要になった場合には、お試しください。

では皆さま、体にはお気を付けて。
お読みいただき、ありがとうございました。