[iOS]シェルスクリプト使ってinHouse版ビルドを自動化してみた


現在、ローカル開発、テストフライト用、本番用・・とビルドの種類がたくさんあって、
その度にアイコン置き換えたりBundleID書き換えたり・・もういや！面倒！！
となったけど、jenkinsとかCircleCIとか、入れてみたいけど導入する余裕もなかったので、
とりあえずコマンドラインを記述できちゃうシェルでできるところまでやってみようと、ビルドしてみた記録です。

## やりたいこと
- 引数でビルドの種類を切り替えたい
- アイコンを置き換えたい
- BundleIDを書き換えたい
- Destributionを設定したい
- アプリ名を書き換えたい
- ProvisioningFileの設定を書き換えたい
- Debug or Release ビルドを切り替えたい
- バージョンを引数で切り替えたい（※未）
- バージョン名のフォルダを自動作成して、そこにipaファイルをはき出したい

要はビルドの度にちまちまやっていた作業をコマンド一発で終わらせたーい！！ってことです

私がシェルのファイルを作るときは、
とりあえずコメントでやりたいことをつらつらかきました。
実際それに沿ってコマンドをかくだけなんですよね。


## 必要になったシェルの超基礎知識
「シェルファイルはコマンドをかいて一気に実行してくれる」くらいの認識で、
私自身シェルを書いたこともない人だったので、文法いろいろ調べながらやりました。
これだけ知ってればかけます。

### 1.とりあえずファイルの最初にこれをかく
`#!/bin/sh`
これがないと動きません。
シェルの実行ファイルにたどり着いてくれます。

### 2.とりあえずファイルの拡張子に.shをつけて保存しよう
今回は `build.sh` で保存しました

### 3.実行するときのコマンド
ファイルのある位置まで`cd`で移動したのち
`$ ./build.sh`
で実行できます。

permissionがどーのこーのいってきたら、
`$ chmod +x build.sh`
とやってあげると実行権限が付与されます。

### 4.引数について
実行時、半角スペースをあけて引数を設定できます
`$ ./build.sh 引数1 引数2`

設定した引数は、特殊な変数で引っ張ってこれます
上の例でいうと、
`$1` は 引数1
`$2` は 引数2 ...
というように使えます。わかりやすい！

`$#`なら、引数の数（上の例なら2が返る）を取得できます。

### 5.コメントアウトは`#`
忘れて`//`とか普通にかいてました。
シェルでは`#`ですよー

### 6.if文の書き方

```
if [ ${1} = "dev" ]; then
    db="iOS Developer"
else 
    db="iPhone Distribution: ****** inc."
fi
```

文字列の一致なら `=` でいけます。
ちょっと特殊な書き方ですが上の例を見ればもう大体わかるのでは？

他にも、こんな書き方します。
`if [ 1 -eq 1 ];` は `if(1 == 1)`
`if [ 1 -ne 1 ];` は `if(1 != 1)`
`if [ 1 -lt 1 ];` は `if(1 < 1)`
`if [ 1 -gt 1 ];` は `if(1 > 1)`

もっと知りたい方は[参考サイト](http://shellscript.sunone.me/if_and_test.html)をご覧になってくださいな

### 7.caseの書き方
```
case "$1" in
    "dev" ) ID="dev.******" ;;
    "dev01" ) ID="inhouse.******.dev01" ;;
    "dev02" ) ID="inhouse.******.dev02" ;;
    "ih-release" ) ID="inhouse.******" ;;
    "release" ) ID="******" ;;
    * ) echo "Error: このビルドタイプは設定されていません。"
        exit 1 ;;
esac
```

もし`$1`と一致する文字列が`"dev"`だったら`ID`に`"dev.******"`を入れてねっていう文です。
`*)`は、if文でいうelse、それ以外と指定するときに使えます。


### 8.途中で引数がとれる！ Read について
```
echo "ARE YOU READY? この設定で実行してよろしいですか？ [yes/no]"
read answer
case "$answer" in
yes)
  echo "COOOOOOOOOOOOOOOL!"
  : OK
  ;;
*)
  echo "OMG! See you again!lol"
  exit 1
  ;;
esac
```

操作しているユーザーに対して応答型で条件をかえたいときに`Read`を使います。
上だと、`yes`以外を返されたら処理を中断します。


### 9.あ、忘れてました・・変数について
基本宣言とかいりません。いきなりかいてセットできます。
`projPath="/Users/****/****"`

参照するときはこれ
`$projPath`
`${projPath}`
違いはスラッシュとかの解釈です。詳しくは↓
[シェルプログラミングの基礎知識](http://www.gadgety.net/shin/tips/unix/shell.html)

## 本題のビルド設定
なんやかんやコメント読めば大体わかるんじゃね？ってことでコード全部のっけます。
※今回本番ビルドの設定は未実装です。あくまでinHouse版のipaファイル出力まででございます。

1. 引数により、もろもろの値の定義設定
2. その値をechoで出力しつつ、確認をひとつはさむ
3. 実行
4. 設定を戻して何事もなかったかのような開発環境に！

という形になっています。

```
#!/bin/sh

# [iOS]Build自動化シェルスクリプト

##### このファイルの使い方 #####
#
# ■ 引数は下記
# 1)ビルドの種類
# 　dev        # 通常の開発ビルド
# 　dev01      # inhouse 開発環境01
# 　dev02      # inhouse 開発環境02
# 　ih-release # inhouse 本番環境
# 　release    # 申請ビルド
# 2)ビルドversion ※現状フォルダ作成にしか使われていない
# 　1.6.0
# 　1.6.0.1
# 　1.6.1 .. etc
#
# ex)コマンド例
# $ ./build.sh dev01 1.6.0
#
# ■ projPath / ipaPath は、自分のローカル環境に合わせて要書き換え
# 　projPath…*******.xcodeprojが存在するひとつ上のフォルダを指定
# 　ipaPath…ipaファイルを出すてっぺんのフォルダを指定（直下に自動的にversion名のフォルダが作られる
#
############################

##### バリデーション #####

# 引数バリデーション
if [ $# -ne 2 ]; then
  echo "Error: 指定された引数は$#個です。" 1>&1
  echo "実行するには2個の引数が必要です。" 1>&1
  exit 1
fi

##### ここから定義設定 #####
echo "****** START DEFINE ******"

# プロジェクトのファイルパス ※ここは自分用に変更してね
projPath="/Users/****/****"

# はき出し先ファイルパス ※ここは自分用に変更してね
ipaPath="/Users/****/****"

# ターゲット名
projectName="*******"

# imgPath iconの置き換えに使う
imgPath="${projectName}/Images.xcassets/AppIcon.appiconset/"
  
# BuindleIDのプレフィックス部分
prefix="com.*****"

# プレフィックスより後ろのBundleIDの定義
case "$1" in
	"dev" ) ID="dev.******" ;;
	"dev01" ) ID="inhouse.******.dev01" ;;
	"dev02" ) ID="inhouse.******.dev02" ;;
	"ih-release" ) ID="inhouse.******" ;;
	"release" ) ID="******" ;;
	* ) echo "Error: このビルドタイプは設定されていません。"
		exit 1 ;;
esac
echo "BundleID は ${prefix}.${ID} に設定されました。" 

# Distribution名の定義
if [ ${1} = "dev" ]; then
	db="iOS Developer"
else 
	db="iPhone Distribution: ****** inc."
fi
echo "Distribution は ${db} に設定されました。" 

# アプリ名の定義
case "$1" in
	"dev" )  appName="**-dev" ;;
	"dev01" ) appName="**-dev01" ;;
	"dev02" ) appName="**-dev02" ;;
	"ih-release" ) appName="**-r" ;;
	"release" ) appName="******" ;;
esac
echo "アプリ名 は ${appName} に設定されました。"

# provioningFileの定義
case "$1" in
	"dev" )  pf="Automatic" ;;
	"dev01" ) pf="********-****-****-****-************" ;;
	"dev02" ) pf="********-****-****-****-************" ;;
	"ih-release" ) pf="********-****-****-****-************" ;;
	"release" ) pf="********-****-****-****-************" ;;
esac
echo "プロビジョニングファイル名は ${pf} に設定されました。"

# Debug or Release の定義
case "$1" in
	"dev" )  mode="Debug" ;;
	"dev01" ) mode="Debug" ;;
	"dev02" ) mode="Debug" ;;
	"ih-release" ) mode="Release" ;;
	"release" ) mode="Release" ;;
esac
echo "ビルドのモードは ${mode} に設定されました。"

echo "バージョンは ${2} に設定されました。"

echo "****** END DEFINE ******"

# 一度確認
echo "ARE YOU READY? この設定で実行してよろしいですか？ [yes/no]"
read answer
case "$answer" in
yes)
  echo "COOOOOOOOOOOOOOOL!"
  : OK
  ;;
*)
  echo "OMG! See you again!lol"
  exit 1
  ;;
esac


##### ここから実行コマンド ######

# プロジェクトファイルまで移動
cd $projPath
echo "プロジェクトファイルまで移動しました"

## plistの変更
# bundleID
sed -i .bk -e "s/${prefix}.dev.******/${prefix}.${ID}/g" ${projectName}/${projectName}-Info.plist
echo "BundleIDを変更しました"

# icon(ロゴ)の変更
cp -f "${imgPath}"ico-60@3x_"${1}".png "${imgPath}"ico-60@3x.png
cp -f "${imgPath}"ico-60@2x_"${1}".png "${imgPath}"ico-60@2x.png
cp -f "${imgPath}"ico-40@2x_"${1}".png "${imgPath}"ico-40@2x.png
cp -f "${imgPath}"ico-29@2x_"${1}".png "${imgPath}"ico-29@2x.png
echo "iconのファイル名を変更しました"

# 全部のビルドをクリーンする
xcodebuild clean -project ${projectName}.xcodeproj
echo "クリーンビルドしました"

# ビルド mode, destribution, target名 を指定
xcodebuild -sdk iphoneos\
	-project "${projectName}.xcodeproj/" -target "${projectName}"\
	-configuration "${mode}" build \
	CODE_SIGN_IDENTITY="${db}" \
	PROVISIONING_PROFILE="${pf}"
echo "ビルドしました"

# ipaファイルの保存先フォルダを作る
mkdir "${ipaPath}"/"${2}"

# .appをパッケージして.ipaファイルを作る
xcrun -sdk iphoneos PackageApplication \
  build/"${mode}"-iphoneos/"${projectName}".app \
  -o "${ipaPath}"/"${2}"/"${appName}".ipa
echo "ipaファイルを作成しました"

# plistを元に戻す
mv -f ${projectName}/${projectName}-Info.plist.bk ${projectName}/${projectName}-Info.plist
# 画像を元に戻す
cp -f "${imgPath}"ico-60@3x_dev.png "${imgPath}"ico-60@3x.png
cp -f "${imgPath}"ico-60@2x_dev.png "${imgPath}"ico-60@2x.png
cp -f "${imgPath}"ico-40@2x_dev.png "${imgPath}"ico-40@2x.png
cp -f "${imgPath}"ico-29@2x_dev.png "${imgPath}"ico-29@2x.png

echo "ALL FINISH BABY! ENJOY YOUR DEVELOPMENT!! haha"

exit 0
```

いくつかポイントだけ解説します。

### プロビジョニングプロファイルについて

```
# provioningFileの定義
case "$1" in
    "dev" )  pf="Automatic" ;;
    "dev01" ) pf="********-****-****-****-************" ;;
    "dev02" ) pf="********-****-****-****-************" ;;
    "ih-release" ) pf="********-****-****-****-************" ;;
    "release" ) pf="********-****-****-****-************" ;;
esac
```

この指定には、プロビジョニングプロファイルのUUIDを知る必要があるのですが、
ちょっと知るのに苦労しました。

`$ cd ~/Library/MobileDevice/Provisioning Profiles`
ここで`ls`すると、UUIDがファイル名になっているプロビジョニングプロファイルがずらっと表示されます。

`ls -lt` とか打って、更新日付あたりから目的のプロファイルを割り出しましょう。

参考：[Finding the UUID of an iPhone provisioning profile](https://coderwall.com/p/znqi-w/finding-the-uuid-of-an-iphone-provisioning-profile)

### plistの書き換え作業でやっていること

[sedコマンド](https://hydrocul.github.io/wiki/commands/sed.html)は、テキスト処理のコマンドです。
`sed -i .bk -e "s/${prefix}.dev.******/${prefix}.${ID}/g"`

- `s/①/②/g` ①を②に置換します
- .bkファイルとして、置換前のファイルをバックアップとってます
- `s` 正規表現で置換する
- `g` 全行で置換する

plistを戻すときは、.bkのついたバックアップファイルを強制上書きしています。
`mv -f ${projectName}/${projectName}-Info.plist.bk`

参考：[iPhoneアプリ開発でBundle IDを書き分けてビルドする方法](http://lab.sonicmoov.com/smartphone/build-change-bundle-id/)


### アイコンについて
若干無理やり作ったアイコンの書き換えですが、
要は4サイズ分 × ビルドの種類分 用意して、
リネームして強制上書きコピーしてるだけですw
戻すときに開発用のアイコンで、再上書きしてあげましょう。



以上、ちょっと面倒だけど一度やってしまえばとても楽になる、
ビルド自動化のご紹介でした。
小さいところから効率化はかっていきましょう〜