Jenkinsパイプラインスクリプトで簡単！Unity＋iOSビルド環境構築

本記事は[サムザップ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/sumzap)の2日目の記事です。
昨日は@norimatsu_yusukeさんの「[ReSharperのテンプレート機能について](https://qiita.com/norimatsu_yusuke/items/94b2f8f9eccbed5f162c)」でした

#はじめに
従来のJenkinsでは複数のジョブを作成し、それらを組み合わせてビルド環境を構築していましたが、Jenkins2ではGroovyスクリプトで実行できるようになり、以前に比べてビルド環境のセットアップが簡単になりました。

そこで、この記事ではJenkins2のPipelineスクリプトを使った、Unity+iOSビルド環境の構築について説明します。

※Pipelineの基本的な使い方については、[こちらの記事](https://qiita.com/miyasakura_/items/9d9a8873c333cb9e9f43)が参考になると思います

#環境
- Mac（OSX Sierraで動作確認）
- Jenkins2.x（v2.7.3で動作確認）
- Xcode（普段お使いのもの）
- Unity（普段お使いのもの）
- Git（普段お使いのもの）

#事前準備
まずはJenkinsを動かすPC上で下記の項目（コマンドライン、Unity、XcodeでのGitアクセスからビルド、ipa作成、キーチェーンでの認証情報設定）が正常に動作することを確認してください。
**基本的に、これらがすべて動作確認できてからJenkinsのコマンドに落とし込んでいきます。**
・・・というか、ここができていないと上手くいかない時に無駄に悩むことになりますので、事前にしっかり準備しましょう
##◆あらかじめ準備しておくべき項目
- Jenkins2のインストールとワークスペースディレクトリの確認
- gitコマンドでプロジェクトのリポジトリから正常にチェックアウトできること
- Unityエディター上でiOS向けにビルドできること
- ipaビルド用plistファイルの準備
 - こちらの記事（[iOSアプリのビルドスクリプトを作成](https://qiita.com/rsahara/items/024d9907af2c051f3d44)）を参考にビルド用のplistファイルを作成しておく
- AppStore関連の証明書設定
- Xcodeでビルドしたプロジェクトからアーカイブできること
- Xcodeでアーカイブファイルからipaファイルを作成できること
- 環境に合わせてパラメーターを設定したJenkinsスクリプトの用意
 - （[サンプルのJenkinsFileをGitHubに置きました](https://github.com/yosizo/TestJenkinsScript)）ので、こちらを参考に自分の環境に合わせてスクリプトを修正してください。

ここまで準備できたら（まぁこれだけでも結構大変ですが…）ようやくJenkinsでの環境設定に移ります。

#ビルドパイプラインの作成
・Jenkinsに管理者権限でログインします
・Jenkinsのメニューから「新規ジョブ作成」でパイプラインを作成
<img width="400" alt="sc1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/22207/fb6f9f1d-7301-dfd7-5c7c-82a13ce864ba.jpeg">

<img width="400" alt="sc2.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/22207/0398b473-2ab3-2d77-cd0f-4efcc945f26a.jpeg">


・パイプラインの設定画面で下記項目を設定する

・同時ビルドの制限
General＞Do not allow concurrent builds にチェック
（**Unityのビルドコマンドは同じプロジェクトに対して複数実行することはできませんので、同時にビルドが走らないようあらかじめ制限をかけておきます**）
<img width="400" alt="sc3.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/22207/5c11ccc5-ffd1-91b6-9420-a3a1afaab08e.jpeg">

・pipeline scriptのテキストボックスにJenkinsFileスクリプトをコピーする

・スクリプトの実行制限を外す
  パイプライン＞UseGroovySandboxのチェックを外す
<img width="400" alt="sc4.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/22207/fa6dc33e-6724-79df-e81e-17aa96bdbfba.jpeg">


#Jenkinsスクリプトの設定
ここでは、[Jenkinsスクリプトのサンプル](https://github.com/yosizo/TestJenkinsScript)をベースに内容を解説していきます。

※*スクリプト中のプロジェクト名やGitリポジトリ、Xcodeビルド用の各種設定項目は、それぞれの環境によって変わってきますので、必要に応じて書き換えてください。*


##1・Inputステップでブランチを選択
まずはビルド対象となるブランチを選択します。
パラメーター付きビルドでやってもいいのですが、ブランチの状態を最新に保つためには管理画面でScriptを動かす等の設定が必要なので今回はInputステップでメニューを表示することにします。

```groovy
def buildBranchName

stage('InputParam')
{
    // リモートからブランチの一覧を取得しプルダウンリスト用に整形する
    def branchList = sh(script: "git ls-remote --heads ${repositoryURL} | sed -e 's#.*refs/heads/##'", returnStdout: true)

    // 入力待ち状態で5分経過したらタイムアウトする
    timeout(time:5,unit:"MINUTES")
    {
        def defaultBranch = "develop"

        // デプロイセッティングの入力待ち
        def inputParam = input(
            message: 'ビルドパラメーターをセットしてください',
            ok: 'ビルド開始',
            parameters: [
                [$class:"ChoiceParameterDefinition",
                    name: 'branchName',
                    choices: branchList,
                    defaultValue: defaultBranch,
                    description: 'ビルド対象のブランチを選択してください'
                ],
            ])

        // 取得したブランチ名を変数に保存
        buildBranchName = inputParam.branchName
    }
}
```

**ChoiceParameterDefinition** クラスでプルダウンメニューを表示していますが、改行で区切られた文字列を渡す必要があるので、[こちらの記事のやり方で一覧を取得しています](https://qiita.com/yosizo@github/items/396247daa6b2dae85e2e)

またここでは、入力待ちの状態で他の人がビルドできなくなることを防ぐため、timeoutブロックを利用して一定時間が経過したら自動的にビルドが失敗するように設定しています。

```
timeout(time:5,unit:"MINUTES")
{
（何かの処理）
}
```

##2・Gitからブランチをチェックアウトする
次にGitステップを使ってブランチからチェックアウトします。
*※お使いの環境に合わせて証明書などの設定は済ませておいてください。*

```groovy
// ビルド対象リポジトリ
def repositoryURL = 'https://[your_build_target_project_repository].git'

// ローカルリポジトリディレクトリ名
def repositoryName = "[your_repository_name]"

stage('Git') {
    dir (repositoryName) {
        // ブランチをチェックアウト
        git( branch: buildBranchName, url: repositoryURL)
    }
}
```

##3・Unityバッチビルドを行う
基本的にこのステージではバッチビルド用のコマンド文字列を生成し、シェルステップで実行しているだけです。

Unityのバッチビルドモードを利用してビルドを行うためには、事前にUnityプロジェクト側でバッチビルド用のクラスを追加する必要があります。
参考：
[コマンドラインからUnityのビルドを走らせる。 -基礎編-](https://qiita.com/sango/items/474efb4c016a136c84ce)
[UnityのBatchBuildについて](http://yutakaseda3216.hatenablog.com/entry/2015/12/07/221714)
[Unityのプロジェクトをコマンドラインからビルドする]
(https://tyfkda.github.io/blog/2015/07/04/unity-build-from-shell.html)


このビルドコマンドについては、プロジェクトによって大きく変わる部分だと思いますので、**まずはJenkinsを動かすPCのコマンドラインでバッチビルドができることを確認し**、そのコマンドを元にJenkins側のスクリプトを書き換えるのが効率が良いと思います。


```groovy
// Unityプロジェクト名
def projectName = "[your_project_name]"

// GitステップでチェックアウトされるUnityプロジェクトのパス
def projectPath = "${WORKSPACE}/${repositoryName}/${projectName}"

stage('Unity Build')
{
    // UnityコマンドPath
    def unityPath = "/Applications/Unity/Unity.app/Contents/MacOS/Unity"
    // Unityビルドスクリプトクラス名
    // ※ここはUnity側に設置したバッチビルド用クラスとメソッド名を指定してください
    def executeMethod = "BuildBatchClass.BuildMethod"
    // ビルドターゲット
    def buildTarget = "ios"

    // UnityのバッチビルドでXcodeプロジェクトをビルドするためのコマンド文字列を作成
    def buildCommand = "${unityPath} -quit -batchmode"
    buildCommand += " -executeMethod ${executeMethod}"
    buildCommand += " -projectPath ${projectPath}"
    buildCommand += " -buildTarget ${buildTarget}"

    // ビルドコマンドを実行
    sh(script: buildCommand)
}
```

##4・XCodeでビルドする
UnityBuildと同様、基本的にこのステージではビルド用のコマンド文字列を生成し、シェルステップで実行しているだけです。

参考：
[iOSアプリのビルドスクリプトを作成](https://qiita.com/rsahara/items/024d9907af2c051f3d44)
[Xcodeでのビルドを自動化するxcodebuildコマンドとIPAファイルを作成してiTunes Connect(Testflight)に投げる方法](http://blog.ch3cooh.jp/entry/20150210/1423573065)
[macOSアプリのCI事情5 – xcodebuildでxcarchiveを作る](http://nantekottai.com/2016/10/11/mac-app-ci-5/)

```groovy
stage('xcode build')
{
    // XcodeプロジェクトPath
    def xcodeprojDir = "${projectPath}/iOS/"
    def xcodeproj = "${xcodeprojDir}Unity-iPhone.xcodeproj"

    // 設定項目（必要に応じて適宜設定してください）
    def bundleID = "xxxxxxxxx"
    def displayName = "xxxxxxxxxxxxx"
    def versionNumber = "xxxxxxxxxxxxx"
    def BuildNumber = "xxxxxxxxx"

     // Plistbuddyで設定項目を書き換える
    sh("/usr/libexec/Plistbuddy -c \"Set CFBundleIdentifier ${bundleID}\" ${xcodeprojDir}Info.plist")
    sh("/usr/libexec/Plistbuddy -c \"Set CFBundleDisplayName ${displayName}\" ${xcodeprojDir}Info.plist")
    sh("/usr/libexec/Plistbuddy -c \"Set CFBundleShortVersionString ${versionNumber}\" ${xcodeprojDir}Info.plist")
    sh("/usr/libexec/Plistbuddy -c \"Set CFBundleVersion ${BuildNumber}\" ${xcodeprojDir}Info.plist")

    // ビルド環境をクリーン
    sh(script: "xcodebuild clean -project ${xcodeproj}")

    // XcodeBuildコマンドでアーカイブ作成
    def developmentTeamName = "xxxxxxxxxxxxxx"
    def provisioningProfilerSpecifier = "xxxxxxxxxxxxxxx"

    sh(script: "xcodebuild archive -project ${xcodeproj} -archivePath ${archivePath} -configuration Develop -scheme Unity-iPhone -sdk iphoneos10.3 CODE_SIGN_IDENTITY=\"iPhone Distribution\" PROVISIONING_PROFILE_SPECIFIER=${provisioningProfilerSpecifier} DEVELOPMENT_TEAM=${developmentTeamName}")
}
```

ここでは**PlistBuddy**というOSXのツールを使ってXcodeのinfo.plistファイルの内容を書き換えています。
アプリの表示名やバージョンなどinfo.plistの項目をビルドごとに変更したい場合はここで行うのが簡単でしょう。
参考：[PlistBuddyの使い方](https://qiita.com/y-okubo/items/df31860897e1717a1314)

```
sh("/usr/libexec/Plistbuddy -c \"Set CFBundleIdentifier ${bundleID}\" ${xcodeprojDir}Info.plist")
```

また、ビルドコマンドを動作させるために**DEVELOPMENT_TEAM**などの値が必要になりますので、下記のコマンドで取得してください。

```
xcodebuild -showBuildSettings -project {プロジェクト名}.xcodeproj
```

参考：
[[Xcode] CIなどを使用するときに必要な値を確認する方法](https://qiita.com/Akira_Kido_N/items/25ffd68cade64ecade2e)



##5・ipaファイルのビルド

```groovy
stage('ipa export')
{
    // ipaビルド設定ファイルPath
    def exportOptionFile = "${WORKSPACE}/build/buildparams/exportOptions.plist"

    // XcodeアーカイブPath
    def archivePath = "${projectPath}/iOS/build/${projectName}.xcarchive"

    // ipa出力先
    def exportDir = "${WORKSPACE}/export/${BUILD_NUMBER}/"
    def exportFileName = "${projectName}_${BUILD_NUMBER}.ipa"

    // exportディレクトリがなければ作成
    dir ('export') {
        // XcodeBuildコマンドでipa作成
        sh(script: "xcodebuild -exportArchive -archivePath ${archivePath} -exportPath ${exportDir} -exportOptionsPlist ${exportOptionFile}")

        if( fileExists("${BUILD_NUMBER}/Unity-iPhone.ipa") )
        {
            // そのままだと毎回、Unity-iPhone.ipaというファイルが生成されるので、アーカイブ用にリネーム
            sh(script:"mv ${BUILD_NUMBER}/Unity-iPhone.ipa ${BUILD_NUMBER}/${exportFileName}")

            // アーカイブ
            archiveArtifacts(artifacts: "${BUILD_NUMBER}/${exportFileName}", onlyIfSuccessful: true)
        }
    }
}
```

##6・おまけ／Chatworkへの投稿
ビルドが完了したことをChatworkに投稿したい場合は以下の記事を参考にステップを追加してください
[Jenkins2のPipelineScriptでChatworkに投稿する
]
(https://qiita.com/yosizo@github/items/a9b98094f014657f7384)
※ビルド番号やスクリプトの結果などをメッセージに追加しておくと読みやすくなると思います。

#まとめ
いかがでしたでしょうか。
一度環境さえ作ってしまえばコピ＆ペーストで簡単にビルド環境を複製できますし、ビルドのパラメーターもGroovyスクリプトで設定できますので（Jenkins職人がいなくても）状況に合わせたビルド環境を複製できます。

また、今回の記事ではJenkinsFileを管理画面から設定しましたが、GitリポジトリからJenkinsFileを取得する事も可能ですのでぜひチャレンジしてみてください。

開発中によくある「これ、実機で動作確認したいんだけど・・・」で作業を中断される悩みから解放され、快適なCIライフを送りましょう。

#さいごに
**「パイプラインスクリプトで簡単と言ったな。」**


**「あれは嘘だ。」**


----
明日はSumzapアドベントカレンダー3日目、@yos316さんの[【Unity】SimpleAnimationについて](https://qiita.com/yos316/items/3c0df9c296d7cf4aa605)です。