はじめに
研究でiOSデバイスを3~4台使うアプリケーションを開発することになったのですが、以下の手順を手作業でやるのは流石にしんどいと思ったので、2~5までを自動化しました。
- 普段使いのWindows PCからGitHubへ変更をpush
- 手元のMac PCでそれをpull
- UnityプロジェクトをビルドしてXcodeプロジェクトの作成
- Xcodeプロジェクトををビルドしてipaの作成
- ipaを手元の実機3~4台へインストール
1'. push時のGitHubからのWebhookをUltraHookでトンネリングしてローカル環境のMac上のJenkinsに通知する
6. ビルドが終了したタイミングでSlackに成否を通知
この記事は自分の備忘録的な意味合いが強いので、全てを詳細に説明することは出来ませんが、代わりに参考記事へのリンクをちょくちょく張っていきます。
要件
- 実機(Windows, Mac, iOSデバイス)が全て手元にあり、MacとiOSデバイスを有線接続できる
- 開発時は実機の全てが常に手元にあるわけではない(開発環境がLAN内で完結できない)
- 出先でWindowsのノートPCで開発するとかの場面がある
- LAN内で完結するならこの記事が参考になるかもです
- 自動化は無料で実現したい(個人開発)
- 有料で良いならdeploygateとか使うとよさげ?
環境
- Windows 10 Home 1909
- Unity 2018.4.20f1
- Mac mini (2018) ... ビルドサーバ
- OS: macOS Mojave (10.14.6)
- CPU: Intel Core i7
- メモリ: 16GB
- Mac software
- Xcode 10.3
- Homebrew 2.2.12
- Jenkins 2.230
- npm 6.14.4
- ios-deploy 1.10.0
- gem 2.5.2.3
- UltraHook client 0.1.5
- iPod touch 7th (3台)
- version: 13.4
環境構築のための手順
自分の辿った環境構築の手順です。記事も以下の流れで解説します。
- Unityプロジェクトの手動ビルドとipaファイルの作成
- Unityプロジェクトからipaファイルを作成し実機へインストールするまでをコマンドラインから行う
- Jenkinsのインストールと初期設定
- Jenkinsのジョブの作成と設定
手順
1. Unityプロジェクトの手動ビルドとipaファイルの作成
ipaファイルとはiOSで動作するバイナリファイルです。
1-1. Unityプロジェクトの作成とGitHubへのアップロード
まずMacでUnityプロジェクトを作成し、GitHubに上げておきます。
この時、自分の環境ではリモートリポジトリへのパスがHTTPSだと後々Jenkinsからのgit pullが失敗しました。なのでGitHubに公開鍵を登録しておき、リモートリポジトリへのパスはgit@github.com:[ユーザID]/[リポジトリ].git
のようにSSHのものにしておきましょう。
1-2. Unityプロジェクトの手動ビルドと実機へのインストールテスト
次にUnityプロジェクトをiOS向けにビルドします。この時、Unity側でXcodeのSigning
情報を設定しておきましょう。
ビルドしてできたXcodeプロジェクトを開いたら、Identity
やSigning
を必要に応じて設定して実機向けにビルド&インストールします。
なお、自分の環境ではXcodeがiOS 13.4をサポートしていなかったので自分でサポートファイルをダウンロードしました。
【Xcode】実機で動かそうとしたらこんなエラーが出た時
1-3. Xcodeプロジェクトからipaファイルの作成
ipaファイルを作成するためには一旦XcodeプロジェクトからArchiveファイルというものを経る必要があります。
この段階で手動ではビルドできているはずなので、XcodeでProduct > Archive
を選択しipaファイルを作成します。
2. Unityプロジェクトからipaファイルを作成し実機へインストールするまでをコマンドラインから行う
ipaファイルの作成までは以下の記事が非常に参考になりました。
UnityのiOSビルドをコマンドラインから行う (Mac用)
スクリプトの大筋は記事と変わりませんが、自分は最終的に以下のようになりました。
2-1. Unityプロジェクト⇒Xcodeプロジェクト
C#スクリプト
以下のスクリプトをAssets/Scripts/Editor/
以下に置きます。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
public class ApplicationBuilder
{
public static void Build()
{
const string outputDirKey = "-output-dir";
var args = Environment.GetCommandLineArgs();
var locationPathName = GetArgumentValue(args, outputDirKey);
var options = new BuildPlayerOptions();
options.scenes = EditorBuildSettingsScene.GetActiveSceneList(EditorBuildSettings.scenes);
options.locationPathName = GetArgumentValue(args, "-output-dir");
options.target = BuildTarget.iOS;
options.options = BuildOptions.None;
var buildReport = BuildPipeline.BuildPlayer(options);
if (buildReport.summary.result == BuildResult.Succeeded)
{
Debug.Log("[Success]");
EditorApplication.Exit(0);
}
else
{
Debug.Log("[Failure]" + buildReport);
EditorApplication.Exit(1);
}
}
private static string GetArgumentValue(IReadOnlyList<string> args, string key)
{
var index = args.ToList().FindIndex(arg => arg == key);
var paramIndex = index + 1;
if (index < 0 || args.Count() <= paramIndex)
{
return null;
}
return args[paramIndex];
}
}
シェルスクリプト
以下のスクリプトをUnityプロジェクトの直下に置きます。
ログがGitの管理下に置かれるとJenkinsでgit pull --rebase
する際に邪魔なので、出力先はBuilds/
以下にしました。
また、スクリプトが失敗した際にJenkinsのジョブがすぐに失敗するよう、ファイルの頭には#!/bin/bash -xe
をつけておきます。
Jenkinsで使うシェルスクリプトは-xeつけた方がよかった
#!/bin/bash -xe
echo "Start Unity build"
UNITY_APP_PATH="/Applications/Unity/Hub/Editor/2018.4.20f1/Unity.app/Contents/MacOS/Unity"
UNITY_PROJECT_PATH="./"
UNITY_LOG_PATH="./Builds/build.log"
UNITY_BUILDER_NAME="ApplicationBuilder.Build"
XCODE_PROJECT_PATH="./Builds/iOS"
$UNITY_APP_PATH -batchmode \
-quit \
-projectPath $UNITY_PROJECT_PATH \
-logFile $UNITY_LOG_PATH \
-executeMethod $UNITY_BUILDER_NAME \
-output-dir $XCODE_PROJECT_PATH
if [ $? -eq 1 ]; then
echo "error!! check logfile: ${UNITY_LOG_PATH}"
exit 1
fi
echo "Finish Unity build"
XCODE_PROJECT_PATH=$XCODE_PROJECT_PATH sh build_xcode2ipa.sh
build_xcode2ipa.sh
は後述。
2-2. Xcodeプロジェクト⇒ipaファイル
以下のスクリプトをUnityプロジェクトの直下に置きます。
ここで、自分の環境ではXcodeのAutomatically Sign
をUnityプロジェクトの段階で有効にしていたことで、前述の記事に倣いプロビジョニングファイルのUUIDを指定すると、逆にエラーが出ました。
Xcode 8: xcodebuildで、Automatic Signingに対応する
自分は上記の記事のビルドエラー2に該当したため、UUIDの指定を取り除きました。
#!/bin/bash -xe
echo "Start ipa build"
SCHEME="Unity-iPhone"
PROJECT_PATH="${XCODE_PROJECT_PATH}/${SCHEME}.xcodeproj"
ARCHIVE_FILE="${SCHEME}.xcarchive"
ARCHIVE_DIR="${XCODE_PROJECT_PATH}/archive"
ARCHIVE_PATH="${ARCHIVE_DIR}/${ARCHIVE_FILE}"
IPA_DIR="${ARCHIVE_DIR}/output_ipa"
EXPORT_OPTIONS_PLIST="ExportOptions.plist"
mkdir -p $ARCHIVE_PATH
# ARCHIVE
xcodebuild -project $PROJECT_PATH \
-scheme $SCHEME \
archive -archivePath $ARCHIVE_PATH
# ipaファイルの作成
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH \
-exportPath $IPA_DIR \
-exportOptionsPlist $EXPORT_OPTIONS_PLIST
echo "Finish ipa build"
IPA_DIR=$IPA_DIR sh install_ipa.sh
ExportOptions.plistに関しては、1項でXcodeのGUIからビルドしたipaファイルと同じフォルダに存在するため、それをシェルスクリプトと同じくUnityのプロジェクト直下にコピーしてきて配置しましょう。
install_ipa.sh
は後述。
2-3. ipaファイルを実機へインストール
作成したipaファイルを実機へインストールするには、**ios-deploy
**というコマンドを利用します。
iOSでコマンドラインで実機にアプリをインストールできる、ios-deployについて
上記の記事に従い、npm
を用いてios-deploy
をMacにインストールしてください。
ios-deployでMacに接続されているiOSデバイスを検出すると、自分の環境では以下のように表示されました。
※ xxxxxx...xxxxxx部分は本来デバイスIDが表示され、yyyyyyyyyy部分は本来デバイス名が表示されます
~ $ ios-deploy --detect
[....] Waiting up to 5 seconds for iOS device to be connected
[....] Found xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (N112AP, iPod Touch 7G, iphoneos, arm64) a.k.a. 'yyyyyyyyyy' connected through USB.
[....] Found xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (N112AP, iPod Touch 7G, iphoneos, arm64) a.k.a. 'yyyyyyyyyy' connected through USB.
[....] Found xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (N112AP, iPod Touch 7G, iphoneos, arm64) a.k.a. 'yyyyyyyyyy' connected through USB.
ここで、ios-deploy
は以下のコマンドでデバイスを指定してipaファイルをインストールできます。
$ ios-deploy --bundle <path_to_ipa> --id <device_id>
Macに接続されているすべてのiOSデバイスにipaファイルをインストールするため、ios-deploy --detect
で検出したすべてのデバイスIDに関して上記のコマンドを実行するシェルスクリプトを自作します。
以下のスクリプトもUnityプロジェクトの直下に配置します。
#!/bin/bash -xe
echo "Start install"
# Jenkinsのジョブからios-deployを利用するためローカルコマンドへのパスを通す
export PATH=/usr/local/bin:$PATH
source ~/.bashrc
# $IPA_DIRはbuild_xcode2ipa.shから引き継ぐ
cd $IPA_DIR
IPA_FILE=`ls *.ipa`
ID_LIST=($(ios-deploy --detect | grep "iPod touch" | awk '{ print $3 }'))
for i in ${ID_LIST[@]}
do
ios-deploy --bundle $IPA_FILE --id $i
done
echo "Finish install"
まとめると、ApplicationBuilder.cs
, build_unity2xcode.sh
, build_xcode2ipa.sh
, ExportOptions.plist
, install_ipa.sh
等は以下のようなディレクトリ構成になります。
UnityProj/
├ Assets/
│ └ Editor/
│ └ ApplicationBuilder.cs
├ Builds/
│ └ build.log
├ build_unity2xcode.sh
├ build_xcode2ipa.sh
├ ExportOptions.plist
└ install_ipa.sh
この状態でsh build_unity2xcode.sh
をターミナルから実行すればUnityプロジェクトのビルドから実機へのインストールまで良しなにやってくれるはずです。
3. Jenkinsのインストールと初期設定
3-1. HomebrewでJenkinsをインストール
Homebrew
を使ってmacOSにJenkinsを直接インストールします。
$ brew update
$ brew upgrade
$ brew install jenkins-lts
ここで、Jenkinsはデフォルトだとインストールされたマシンからのアクセスしか受け付けないのですが、自分はWindowsのブラウザからJenkinsを操作できるようにしたかったため、バックアップを取ったうえで設定ファイルを書き換えました。ついでに念のため、他のアプリでも何だかんだよく使われるPort 8080
もPort 9000
に変更しておきました。
参考:macOS上でJenkinsを自動起動させる
変更前
...
<string>--httpListenAddress=127.0.0.1</string>
<string>--httpPort=8080</string>
...
変更後
...
<string>--httpListenAddress=0.0.0.0</string>
<string>--httpPort=9000</string>
...
その後、以下のコマンドでJenkinsをmacOSの起動と同時に起動するよう設定しました。
$ brew services start jenkins
上記コマンドは同時にJenkinsの起動も行うので、出力が落ち着いたタイミングでhttp://localhost:9000/
へアクセスし、画面に従いセットアップ、プラグインのインストール等を行ってください。
参考:MacにJenkinsインストール
3-2. Jenkinsの初期設定
Slack
JenkinsのジョブとSlackを連携させるため、SlackとJenkinsの初期設定を行います。
まずはSlackのアプリケーション管理画面(https://[チャンネルID].slack.com/apps/manage
)でJenkins CI
を検索しSlackに追加
してください。
その後Jenkins CI
の設定画面で好きなチャンネルにこのアプリを追加すると、ランダムな英数文字列のトークンが発行されるので控えておきます。
今度はJenkinsのメニューからJenkinsの管理 > プラグインの管理
を選択し、検索バーにSlackと打ち込みます。出てきた候補の中からSlack Notificationを選択し、画面に従ってインストール&Jenkinsを再起動してください。
再起動後、Jenkinsの管理 > システムの管理
を選択するとSlackの設定項目が追加されています。Workspace
の項目にはワークスペースのIDを入力し、Default channel / member id
には先ほど設定したSlackのチャンネル名を記述します。
次にCrediential
の追加 ▼ > jenkins
を押すとポップアップ画面が出てくるので設定します。
種類
はSecret text
を選択し、Secret
に先ほど得たSlackのチャンネルへのトークンをコピペします。ID
には好きな名前を入れておいてください。
追加
ボタンを押すとポップアップが消えるので、戻った画面のCrediential
の項目でたった今作成したものを選択しましょう。
最後に画面左下の保存
ボタンを押して完了です。
SSH鍵
JenkinsがGitHubからgit pull
するときに使うSSHの秘密鍵を登録します。
以下の記事が詳しかったので、そちらを参考に設定してください。
【Jenkins】GitのSSH接続をするための認証情報を設定する【GitHub】
Git
自分の環境ではJenkinsがgitコマンドへのパスを持っていなかったので、登録します。
Jenkinsの管理 > Global Tool Configuration
で、gitのパスを以下のように追加します。パス自体は環境によって異なると思うので、コマンドでwhich git
とか打って確認してください。
4. Jenkinsのジョブの作成と設定
GitHubからローカル環境のJenkinsジョブを実行させるにあたり、以下の記事が参考になりました。
GithubからWebhookでJenkinsのジョブを自動実行
ただ、ビルドトリガーをリモートからビルドでなくGitHub hook trigger for GITScm pollingにするともう少し楽ができるので、この記事では後者を使う前提で話を進めます。
4-1. ジョブを作成する準備
UltraHookの設定
GitHubからのWobhookをローカルマシンにフォワードするため、UltraHookというサービスを利用します。
まずはUltraHookのトップページでGet Started Now!
というボタンを押してユーザ登録をします。登録が完了するとAPIキーがもらえるので、Macのルートディレクトリに以下のように保存しましょう
$ echo "api_key: <APIKEY>" > ~/.ultrahook
あとは以下のコマンドで起動できます。(9000はJenkinsのポート番号)
$ ultrahook stripe 9000
Authenticated as <yourId>
Forwarding activated...
http://stripe.<yourId>.ultrahook.com -> http://localhost:9000
GitHub Webhookの設定
Webhookを設定する前に、外部からJenkinsにジョブを実行させるためのユーザトークンを取得します。
Jenkinsの管理 > ユーザーの管理 > [自身のJenkinsユーザIDを選択] > 設定
の中のAPIトークン
の欄でAdd new Token
を押してください。
適当なトークン名を記入したらGenerate
し、表示された文字列をどこかに控えておきます(一度閉じたらもう見れないので)。控え終わったら画面左下の保存
を押してページを抜けます。
参考:Jenkinsのジョブをリモートからの実行する手順
次はGitHubのリポジトリのページでSetting > Webhooks
からAdd webhook
ボタンでWebhookを追加し、Payload URL
に以下のURLを入力します。
http://<JenkinsのユーザID>:<Jenkinsのユーザトークン>@stripe.<UltraHookのユーザID>.ultrahook.com/github-webhook/
注意点として、http://...ultrahook.com/github-webhook
のように末尾にスラッシュを付け忘れるとダメです。スラッシュを付け忘れても302リダイレクトが起こるのですが、どうやらUltraHookはそれを握りつぶしてしまうようで、結果としてGitHub側ではWebhookで200レスポンスが帰ってきているのにJenkinsのジョブは実行されないという現象が起こります。(一敗)
4-2. Jenkinsジョブの作成
ようやっっとJenkinsジョブを作成していきます。ここまで来ればあとはもう少しです。
Jenkinsの新規ジョブ作成
を押し、ジョブ名を入力したらフリースタイル・プロジェクトのビルド
を選択します。
General
General
は以下のように設定します。GitHubのリポジトリへのURLはclone時のものではなく、普通にブラウザのURL欄に表示されてるやつです。
ソースコード管理
ソースコード管理
ではGit
を選択し、clone時のパスを入力します。認証情報は3-2. Jenkinsの初期設定のSSH鍵の項で作成したものを選択してください。追加 ▼
を押す必要はないです。
また、自分はビルドするブランチは特に指定しませんでした。
ビルド・トリガ
GitHub hook trigger for GITScm pollingを選択してください。
ビルド
Unityプロジェクトの直下へ移動し、git_pull.sh
を実行します。このシェルスクリプトには、git pull
から2章で作成したbuild_unity2xcode.sh
を実行する処理までを記述します。
#!/bin/bash -xe
echo "Start git pull"
git fetch origin
# GIT_LOCAL_BRANCHが空の場合はGIT_BRANCHから"origin/"以降の文字列を抽出して代入
GIT_LOCAL_BRANCH=${GIT_LOCAL_BRANCH:-${GIT_BRANCH##origin/}}
# ローカルにGIT_LOCAL_BRANCHという名のブランチが存在する場合は単純にHEADを移動。存在しない場合は作成し移動。
# 参考:https://qiita.com/knknkn1162/items/b3af70918770d85bc313
git checkout $GIT_LOCAL_BRANCH
git pull --rebase
echo "Finish git pull"
sh build_unity2xcode.sh
$GIT_LOCAL_BRANCH
や$GIT_BRANCH
はJenkins側で勝手に設定してくれる環境変数です。ここら辺の環境変数はシェルスクリプト記述欄の下のビルドから利用可能な環境変数の一覧から確認できます。
ここで、pushのあったブランチがローカルに存在しない場合は$GIT_LOCAL_BRANCH
が空欄になるので、スクリプトでは$GIT_BRANCH
から**"origin/"**という文字列を除去したものを代入してcheckoutしています。
参考:git checkout理解してなかった
ビルド後の処理の追加
Slack Notificationを追加していれば、ビルド後の処理の追加 ▼
からSlack Notification
を選択し以下のように設定できます。画像ではすべての通知を有効化しています。
これにより、Slack側で以下のように通知を受け取ることができます。
5. おまけ
さて、以上の作業が完了すればGitHubへのpushをトリガーにiOSアプリの実機インストールまでを自動化することができます。
しかし、現状ではMacを起動するたびにUltraHookクライアントをコマンドから起動する必要があり、また体感一時間くらい何もしてないと、UltraHookクライアントはエラーを吐いてプロセスが終了してしまいます。
上記のタイミングで毎回UltraHookクライアントを起動するのは面倒...というか絶対に忘れそうなので、MacのAutomatorを使って、Macの起動時にUltraHookクライアントが自動で立ち上がるようにしたり、エラー出力時に自動で再接続してくれるようにしました。
※注意
下記の設定を行うと、UltraHookへPOSTリクエストがあってもコマンドプロンプト上でその結果を見ることができないため、場合によってはデバッグがし辛くなります。この設定は1~4章までの手順を完了した後に行うと良いでしょう。
AutomatorでUltraHookの起動と継続を自動化
MacのSpotlight検索でautomatorと打って起動します。
アクションからシェルスクリプト実行
を選んで右側のスペースにドラッグアンドドロップし、以下の内容を記述します。
export PATH=$PATH:/usr/local/bin
while true
do
ultrahook stripe 9000 || continue
done
一行目では、Automatorからシェルスクリプトを実行する際にユーザコマンドへのパスが読み込まれないので、無限ループに入る前にパスを追加してultrahook
コマンドが使えるようにしています。
二行目以降では無限ループを作り、いささか行儀が悪いですがultrahook stripe 9000
コマンドの実行結果をOR演算子で受け取ると無条件でループを再開しています。
参考:シェルスクリプトでエラー時の処理を行う方法
右上の実行ボタンでスクリプトを実行してエラーが出ないことを確認したら、名前を付けてアプリケーション
フォーマットとして保存しましょう。
作成したアプリケーションは、以下の記事を参考にMacへのログイン時に自動で起動するよう設定しておきましょう。
Mac - ログイン時にアプリケーションを自動起動
おわりに
お疲れさまでした。ここまで読んでいただきありがとうございます。
実際に今回の構成を使ってみると、SceneにCubeしか置いてないUnityプロジェクトでもビルドに12分とかかかってるので、自動化して良かったなぁと感じています。
Jenkinsまわりも今まで知識しか持っていなかったので、今回の件で簡単な実践を積めて満足です。
あとはこの環境の構築にかかってしまった時間を、今後の開発で取り返していきたいですね。
余談ですが、最初はなんとなくJenkinsをMac上のDockerコンテナで動かそうとしていたものの、単純に作業量が増えるうえ(今回の件だと)特に恩恵もないことが判明したので途中で諦めました。
JenkinsもDockerもほぼ素人なので、将来的にJenkinsにもっと慣れて、ビルドの並列化とかを考えるようになったら組み合わせていろいろやってみたいですね。