UnityのiOSビルドをJenkinsに任せて、ついでにCrashlytics beta配信して結果をChatWorkに知らせる

■ はじめに
これは 【その2】ドリコム Advent Calendar 2015 6日目の記事です
5日目の記事はi98katさんの「QC(クオリティコーディネーター)???」です。
【その1】ドリコム Advent Calendar 2015 - Adventar はこちら



■ 自己紹介
ID: tanaka0000 です。
ゲームのクライアントエンジニアをしています。



■ 本日のお話
最近社内の色々な人から、UnityのビルドをJenkinsで行いたいと聞くので、
私が主にやっている流れを紹介します。
f:id:tanaka0000:20151204142137p:plain:w300 f:id:tanaka0000:20151204140003p:plain:w200

私も以前はそうだったのですが、Unityユーザーは、意外にGUIからの
手動ビルドをしてXcodeでアーカイブを作ることはやったことがあっても、Jenkinsでの
自動ビルドをしたことがなく難しいというイメージを持っている人が少なくないです。
ただ事前準備は多少必要ですが、ipaを作成するまでのGUIでやっている設定や実行を
shellの3,4回実行に置き換えているだけなので、非常に簡単にしかも一度設定してしまえば
毎回のビルド手順がほぼなくなるので是非やったことがないことは試してみてください。

今回は、記事が長くなりそうなのでiOS限定にしておきます。



■ 今回のお話内容
 - やりたいこと
 - 準備
 - ビルドの流れ
 - ビルド処理内容
 - Jenkinsの設定
 - さいごに



■ やりたいこと
 - gitリポジトリからプロジェクト取得
 - Unityプロジェクトからipaを作る
 - crashlytics beta配信する
 - chatworkに成功失敗を通知する



■ 準備
 - mac pc
 - Unity
 - Xcode
 - Jenkins
 ※1 JenkinsはHomebrewで立てちゃうのが楽かと思いますので、以下を参考にされるといいかもしれません。
   http://qiita.com/makoto_kw/items/cbe93d4ebbc35f3b43fd
 ※2 それ以外はYosemiteであれば初期状態で色々入っているので問題ないですが、
   SDK周りの更新あれば追加をお願いします。



■ ビルドの流れ
 ① gitリポジトリからプロジェクト取得
 ② Unityビルド
 ③ Xcodeビルド処理
 ④ crashlytics beta配信処理
 ⑤ chatwork処理



■ ビルド処理内容
Unityプロジェクトからipaを作る場合、
Unityプロジェクト → Xcodeプロジェクト → アーカイブの流れになります。

では、具体的にJenkinsで何をするかというと
 - Unityプロジェクト -
  shellからUnityプロジェクト内のメソッドを叩いてXcodeプロジェクトを吐き出す
  Unity - マニュアル: コマンドライン引数
 - Xcodeプロジェクト -
  shellからXcodeを叩いてアーカイブ(*.ipa)を作成する



Unityビルド処理
先ほど書いた通り、UnityプロジェクトからXcodeプロジェクトに出力するために、Unityプロジェクト内のビルド用メソッドを叩く形になります。
そのため、Xcode吐き出し用の処理を事前に作っておく必要があります。
その上で、Unityの決まり事ですが以下を注意してください。
==================================================
① ビルドコードは、Editorディレクトリ以下に配置
  → using UnityEditorを使用するため。
② 使用するメソッドは、staticメソッドとして定義
  → エディタスクリプトなので実行時に
    クラスのインスタンスがないということなのですが、
    ほぼお約束だと思ってください。

==================================================

以下がコードの例です。

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Linq;
/// <summary>
/// Jenkinsビルドクラス.
/// </summary>
public class JenkinsBuilder
{
    /// <summary>
    /// iOSビルド.
    /// </summary>
    public static void iOSBuid ()
    {
        // スイッチプラットフォーム.
        if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.iOS) // Unity4系は、BuildTarget.iPhone になります.
        {
            EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.iOS);
        }

        // シーン名を取得する.
        List<string> scenePathList = new List<string>();
        List<EditorBuildSettingsScene> sceneList = EditorBuildSettings.scenes.ToList();
        foreach (EditorBuildSettingsScene scene in sceneList)
        {
            if (scene.enabled && File.Exists(scene.path))
            {
                scenePathList.Add(scene.path);
            }
        }

        // ビルド.
        if (0 < scenePathList.Count)
        {
            BuildPipeline.BuildPlayer(
                scenePathList.ToArray(),
                "hogehoge",
                BuildTarget.iOS, // Unity4系は、BuildTarget.iPhone になります.
                BuildOptions.None
            );
        }
        else
        {
            // 異常終了.
            Debug.LogWarning("ビルドすべきシーンが見つかりません。処理を終了します。");
            EditorApplication.Exit(1);
            return;
        }

        // 正常終了.
        EditorApplication.Exit(0);
    }
}

まず、今回何に対してビルドをしたいのかを
EditorUserBuildSettings.SwitchActiveBuildTargetで
事前にプロジェクト内全体で準備をしておき、
BuildPipeline.BuildPlayerでXcodeへのエクスポートを行います。



Crashlytics beta配信用処理
※ もし、crash解析が必要なければ、この項目の②は必要ありません。

今回は、Crashlytics beta配信を行うため、以下の手順も追加になります。
 ① 一度手動でCrashlyticsのアップを行う(Xcodeで)
 ② crashlyticsに必要コードと設定を追加する



① 一度手動でCrashlyticsのアップを行う(Xcodeで)
  今回は、この部分だけでもかなりの量になってしまうので割愛させてもらいますが、
  他の方の記事でアップ方法は書いてあるかと思いますので、そちらを参照頂ければ幸いです。



② crashlyticsに必要コードと設定を追加する
①を行ってもらうとわかりますが、crashlytics beta配信を行う上で、
以下のことは必ず行う必要があります。
 ・Build Phase に Run Script 処理を追加
 ・Fabric用Framework を追加
 ・AppControllerに追記
 f:id:tanaka0000:20151204103212p:plain:w200f:id:tanaka0000:20151204100659p:plain:w200

これらに関しては、以下Attributeを使うとXcodeプロジェクト吐き出し後にコールされるので
新たにメソッドを定義してその中で編集するといいかもしれません。
PostProcessBuildAttribute Unity-Scripting API

また、Xcodeプロジェクトの編集に関してはUnityが編集用APIを用意してくれています。
PBXProject Unity-Scripting API
PlistDocument Unity-Scripting API
※ このAPIは出たばっかだと思いますので、古いUnityを使用されている方は、
  Assets/Editor以下にPostProcessBuildPlayerというファイルを作成し、
  pythonrubyで編集を行う方法もありますので検索してもらうといいかもしれません。

以下コード
下準備

// xcodeprojパス.
string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";

string projStr = string.Empty;
try
{
    // xcodeproj 読み込み.
    projStr = System.IO.File.ReadAllText(projPath);
}
catch
{
    Debug.LogError("読み込みに失敗しました。処理を中断します。");
    return;
}

// プロジェクト内容適用.
PBXProject proj = new PBXProject ();
proj.ReadFromString(projStr);

// ターゲットの保持.
string target = proj.TargetGuidByName("Unity-iPhone");



Fabric用Framework を追加
(path/to/は変更をお願いします。)

// Framework のパスを追加.
proj.AddBuildProperty(
    target,
    "FRAMEWORK_SEARCH_PATHS",
    "$(PROJECT_DIR)/Frameworks");

// 追加するフレームワーク名.
List<string> frameworkList = new List<string>();
frameworkList.Add("Fabric.framework");
frameworkList.Add("Crashlytics.framework");

// 現在のフレームワーク収納場所.
string srcDirPath = "path/to/";

// コピー先のフレームワーク収納場所.
string dstDir    = "Frameworks/";
string dstDirPath = Path.Combine(path, dstDir);

// コピー先ディレクトリを作成.
if (!Directory.Exists(dstDirPath))
{
    Directory.CreateDirectory(dstDirPath);
}

// フレームワークのコピーと追加.
foreach (string framework in frameworkList)
{
    string srcPath = srcDirPath + framework;
    string dst = "Frameworks/" + framework;
    string dstPath = dstDirPath + framework;
    if (File.Exists(srcPath))
    {
        File.Copy(srcPath, dstPath);
        proj.AddFileToBuild(target, proj.AddFile(dst, dst, PBXSourceTree.Source));
    }
    else
    {
        Debug.LogWarning(srcPath + " にファイルが存在しないため、Xcodeプロジェクトに追加できません。処理をスキップします。");
    }
}

try
{
    // xcodeproj 書き込み.
    File.WriteAllText(projPath, proj.WriteToString());
}
catch
{
    Debug.LogError("書き込みに失敗しました。処理を中断します。");
    return;
}



Build Phase に Run Script 処理を追加

pbxprojをファイル読み込みして、"./Frameworks/Fabric.framework/run <API_KEY> <BUILD_SECRET>"をRun Scriptに挿入、
またはフェーズ追加をし、そのRun Scriptを最後に行われるように順番を入れ替えるように文字列操作を行います。
(もしかしたらUnityEditor.iOS.Xcodeで簡単な方法があるかも?)
これは、Unityのバージョンによって最適な編集方法が異なるので今回は割愛します。



AppControllerに追記
上のようなC#Xcode吐き出しタイミングで、
AppController.mmを直接Sytem.IOで編集していく方法もありますが、
AppControllerをオーバラーライドして、事前に定義しておくことも可能です。
./Assets/Plugins/iOS に MyAppController.mmを追加し、以下を記述します。

#import "UnityAppController.h"
#import <Fabric/Fabric.h>
#import <Crashlytics/Crashlytics.h>

@implementation MyAppController : UnityAppController

-(BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    [Fabric with:@[CrashlyticsKit]];

    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end

IMPL_APP_CONTROLLER_SUBCLASS(MyAppController)



■ Jenkinsの設定
上の処理が終われば、あとはJenkinsの設定で全て完結します。
今回はJenkinsプラグインを使っていこうと思います。

プラグイン導入
 [Jenkins管理] → [プラグインの管理] で以下のものを追加してください。
 Git Plugin - Jenkins - Jenkins Wiki
 Unity3dBuilder Plugin - Jenkins - Jenkins Wiki
 Xcode Plugin - Jenkins - Jenkins Wiki
 ChatWork Plugin - Jenkins - Jenkins Wiki


Jenkins Plugins事前設定
 [Jenkins管理] → [システム設定]
 
 Unity.appのPath設定を指定してください
 f:id:tanaka0000:20151204201410p:plain:w300

 ChatWork API KEY設定を指定してください
 f:id:tanaka0000:20151204201756p:plain:w300



Job設定
Git Plugin
f:id:tanaka0000:20151204165223p:plain:w500
取得先のURLとブランチを設定すればそれだけでおkです。
polling設定等もあるので、使用するとより便利になると思います。



Unity3dBuilder
[ビルド手順の追加] → [Invoke Unity3d Editor] を選択
上のリファレンスを参考にクラス、メソッドなどを適切に設定します。
f:id:tanaka0000:20151204202346p:plain:w800



Xcode Plugin
[ビルド手順の追加] → [Xcode] を選択
Xcodeプロジェクトの場所、証明書、プロビジョニングや出力先など設定します。
f:id:tanaka0000:20151204202635p:plain
f:id:tanaka0000:20151204202642p:plain
f:id:tanaka0000:20151204202652p:plain
Xcodeビルド時にパラメータに、CODE_SIGN_IDENTITYとPROVISIONING_PROFILEを
明示的に設定していますが、複数の証明書やプロビジョニングを
管理していなければ特に必要ないです。
基本的に Code Signing Identity / Embedded Profile に準拠していると思いますが、
なぜか、違うmobileprovisioningでビルドが走り、
アーカイブする時と誤差が出た時があったので
私は明示的に証明書とプロビジョニングは指定するようにしています。

Yosemite以降 ResourceRules.plist: cannot read resources
エラーが出る可能性がありますが、以下を参照するといいかもしれません。
(M様ありがとうございます!)
jenkins - How do we manually fix "ResourceRules.plist: cannot read resources" error after xcode 6.1 upgrade? - Stack Overflow



Crashlytics beta配信
チェンジログなどのせるといいかもしれませんね。
Get access to Build Changelog in Jenkins - Develop Park (Developers Programming Skills Exchange Park)

Crashlytics のAPIを使ってipaを送信.
Beta Distribution with iOS build servers – Fabric

[ビルド手順の追加] → [シェルの実行] を選択
(/path/to/を適正なパスに変更してください。)

source ~/.bash_profile

IPA_PATH=${WORKSPACE}/Build/*.ipa
NOTES_PATH="${WORKSPACE}/ReleaseNotes.txt"
EMAILS="hogehoge@fugafuga.com"
CRASHLYTICS_API_KEY=<API_KEY>
CRASHLYTICS_BUILD_SECRET=<BUILD_SECRET>
GROUP_ALIASES="testers"
NOTIFICATION="NO"

# 以前の更新情報削除.
if [ -e ${NOTES_PATH} ]; then
 rm ${NOTES_PATH}
fi

# 更新情報.
echo "${JOB_NAME} ${BUILD_NUMBER} ${GIT_COMMIT}" > ${NOTES_PATH}
echo "---" >> ${NOTES_PATH}
curl "${BUILD_URL}/api/xml?wrapper=changes&xpath=//changeSet//comment/text()" | gsed -e 's/<changes>//' -e 's/<\/changes>//' -e 's/<changes\/>/none/' -e 's/^/・/g' >> ${NOTES_PATH}

# Crashlyticsへipaを送信.
/path/to/Crashlytics.framework/submit ${CRASHLYTICS_API_KEY} ${CRASHLYTICS_BUILD_SECRET} -ipaPath ${IPA_PATH} -emails "${EMAILS}" -groupAliases "${GROUP_ALIASES}" -notesPath "${NOTES_PATH}" -notifications "${NOTIFICATION}"



ChatWork Plugin
[ビルド後の処理の追加] → [Notify the Chatwork] を選択
送信先の部屋を指定し、成功、失敗、中断など幾つかのパターンがありますので、
必要に応じてチェックボックスを選択しつつ、送信メッセージを入力してください。
f:id:tanaka0000:20151204214640p:plain
すると結果はこんな感じになります。
f:id:tanaka0000:20151205011634p:plain

文字だけもいいですが、このChatWork Pluginはタグもうてるので、
このように画像を活用することができます。
あらかじめ送信先の部屋に成功と失敗の画像をアップしておき、
そのID(今回は 59570556/59570557)を保持しておきます。
あとは、ChatWork Pluginの設定画面のようにタグを仕込むと
結果がわかりやすく表示されます。
もちろん、もっとわかりやすいようにToなどもできますので
いろいろ試してみてはいかがでしょうか。


ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています



■ さいごに
記事が長くなりすぎてしまったにもかかわらず
最後まで読んでくださった方、ありがとうございます。

今回は、ipaのお話をしましたが、apkも当然できますし、
Unityでは必ず付いて回るAssetBundle作成など、
Jenkinsで自動化できることは多々あります。

是非、使ったことがない方は
楽さがわかると思いますので
試してみてください。



明日は、takkjogaさんです。