コマンドラインだけでiOS開発したい人生だった

こんにちは。最近Pixel 4に乗り換えた一介のiOS開発者です。
時間が余ったので、飛び込みでどこかのアドベントカレンダーに参加しようかと思い筆を取りました。
コマンドラインでiOS開発をするには？というテーマで以前からコツコツ環境を整備していたので、共有してみたいと思います。

## 課題 / モチベーション

- Xcodeいろいろ重い。
- 複数Xcodeで同時ビルドもできるが、どうしても遅いので直列実行したい。
- XVimもいいけどやっぱりVimがいい。

## スコープ

アプリのビルドとテスト実行のみです。
確かSimulatorでの実行とコンソールログまでCLIで完結している人をみた事がありますが、自分は知見がないため触れません。知見募集。

## 前提条件

このソリューションを使うとハッピーになれるかもしれない人

- ターミナルが快適すぎてターミナルの外に出ると路頭に迷う人
- XcodeではないCLIで使えるエディタが好きな人(オブラートに包んだ表現)
- とはいえ[Bazel](https://bazel.build)とか試すほど冒険する気はないのでとりあえずxcodebuildでいい人
- (recommended) テスト書いてある人
- (recommended) コードレイアウトしている人
- (recommended) XcodeGen使っている人

UI周りやテストがない部分はSimulatorを立ち上げることになり重い腰を上げてターミナルから離脱しないといけないので、なるべくコードで完結する部分を増やしておくのがポイントです。
xib読めるから平気という~~人間捨てている~~人は頑張って書いてもいいですが、レビューする方が読めない可能性もあるし通常はオススメしません。

## 用意する材料

### xcodebuildスクリプト

XcodeのGUIを使っていると、何もしていなくてもindexingやインクリメンタルビルドなどのコストがかかってくると思います。
`xcodebuild` を使うことのメリットとして、そういったコストはかからず、必要な時に必要な処理を実行する事ができるという点がありますね。
`xcodebuild` を直接使うのにハードルを感じる人は `fastlane` とか使ってみてもいいと思いますが、個人的にはMakefileを書く労力とあんまり変わらないと思ってます。

CLIで `xcodebuild -project App -scheme ...` と書いていくのもまた一興ですが、仕事でビルドする時のパラメータなんて大体いつも一緒だと思うので、Makefileとかスクリプトにしてどこかに置いておきましょう。
パスも含めて短い方がいいので、自分は以下の名前でbashスクリプトを保存しました。

```
~/bin/my-proj/build   # build-for-testing
~/bin/my-proj/test    # test-without-building
~/bin/my-proj/bt      # build && test
```

### ログフィルターツール

定番の `xcpretty` や `xcbeautify` でいいという人はそれでも構いません。
カラフルなログがターミナルを流れていくのを見るのは楽しいですしね。
自分の場合は`sed`や`grep`を使って必要な情報だけが出るようにしておりまして、誰得かもしれませんが、せっかくなので晒しておきます。
最近のiOSのプロジェクトであれば、大体これで拾えると思います。
SwiftLintの出力も拾っていますね。

`tee` で元々のログも保存しているので、拾いきれなかった時も生ログを参照できます。

フィルターを適用した各スクリプトはこんな感じです。

#### ~/bin/my-proj/build

```bash
#!/bin/bash
make build-for-testing 2>&1 \
    | tee build$(date +%s).log \
    | egrep '(swift:[0-9]+:[0-9]+: error:|cannot be found|BUILD SUCCEEDED|.swift.*warning.*Violation)' \
    | sed 's/:/,/g'

exit ${PIPESTATUS[0]}
```

#### ~/bin/my-proj/test

```bash
#!/bin/bash
set +e

TEST_STDOUT_LOG=test$(date +%s).log
make test-without-building 2>/dev/null \
    | grep -v '(One of the two will be used.|^CoreData:)' \
    | tee $TEST_STDOUT_LOG \
    | egrep -A 2 '(encountered an error|Failing Tests|swift:[0-9]+: error:|Test Suite.*\.xctest.*(passed|failed)|xccovreport|TEST EXECUTE|^Fatal error)'
STATUS=${PIPESTATUS[0]}

exit $STATUS
```

#### ~/bin/my-proj/bt

```bash
#!/bin/bash
rm build[0-9][0-9]*log 2>/dev/null
rm test[0-9][0-9]*log 2>/dev/null
set -e
echo "Building..."
~/bin/my-proj/build
set +e
echo "Testing..."
~/bin/my-proj/test 2> /dev/null
```

標準エラー出力を捨てているのは、XVimを入れているとpluginのコード署名に関する警告が出るためです.. 消す方法あるのかな。

### 共有設定 (optional)

これは必須ではないですが、複数のマシンを行き来して開発したり、新しいマシンに交換する時、何も考えなくても共有されていた方が便利です。
Google Driveでもなんでもいいので、ディレクトリ単位でスクリプトを共有できるようにしておくといいと思います。
自分は `~/bin/` のようなよく使うツールがあるところは、iCloud Driveで共有されている`~/Documents/`配下へのsymlinkにしています。

チームで使うのであれば、[cmdshelf](https://github.com/toshi0383/cmdshelf)でスクリプト共有するというのも手ですね。

### waitp

こちらは自作のスクリプトです。
指定されたプロセス(ファイルI/Oを含む処理である事が条件)の完了を待つという汎用的なbashスクリプトです。
関数として `~/.bashrc` などに登録しておくと良いでしょう。

```bash
function waitp {
    pid=$(ps | grep "${1:?}" | grep -v "grep ${1:?}" | head -1 | awk '{print $1}')
    if [ -z $pid ]; then
        return
    fi
    lsof -p ${pid:?} +r 2 > /dev/null 2>&1
}
```

### notifyme

こちらも雑な自作スクリプトで、AppleScript経由で通知センターに通知を飛ばす関数です。(音量注意)

```bash
alias notifyme='echo "display notification with title \"done\" sound name \"Beep\"" | /usr/bin/osascript'
```

## レシピ

以上の材料を組み合わせると、テスト実行のコマンドはこういう感じになります。

```
waitp my-proj/bt; ~/bin/my-proj/bt ; notifyme
```

`my-proj/bt`をプロセス名に含むプロセスの完了を待ってからテストを実行し、終わったら成功失敗関係なく通知します。

ログはこんな感じで、必要最低限しか流れません。

```console
$ waitp my-proj/bt; ~/bin/my-proj/bt ; notifyme
Building...
** TEST BUILD SUCCEEDED **
Testing...
Test Suite 'MyProjTests.xctest' passed at 2019-12-24 12:04:18.192.
	 Executed 4271 tests, with 0 failures (0 unexpected) in 69.011 (70.223) seconds
Test Suite 'All tests' passed at 2019-12-24 12:04:18.205.
--
** TEST EXECUTE SUCCEEDED **
```

## 実演

それでは実際に動かしてみましょう。こんな感じです。

`![時間がなくなってきたので後で貼ります！]()`

`waitp` のおかげでビルドが勝手に直列実行されていますね。
ビルド大体5分くらいはかかるので、2つ終わるまでにコーヒー休憩できちゃいます。

## まとめと今後の展望

以上、オレオレ開発環境の共有でした。

環境を作るのにちょっと手間をかけてしまっているのですが、一日を通してXcodeを立ち上げないで済んだ日は謎の達成感があります。
前提条件でも書きましたが、テストを充実させておくなど、なるべくコードで完結するように環境を整備していくことをオススメします。テスト書くのは品質の向上にもつながりますしね！この際どうでもいいけど！

ちなみにお気づきの方もいると思いますが、エディタの設定は特にいじっていません。つまりSwiftやUIKitなどのコード補完は一切効きません。
[Swiftのlanguage server protocol](https://github.com/apple/sourcekit-lsp)も少し前に出てきましたが、UIKitなどApple SDKの対応がないのが悩みどころですよね。
この辺りは今後の課題です。まあiOS開発に関しては脳内補完がある程度効くので自分は割と事足りちゃってますが..

それでは良いコマンドライン生活を！