Makefileを利用してiOS開発を賢く便利に運用しよう🎉

## はじめに

[VALU Advent Calendar 2019](https://qiita.com/advent-calendar/2019/valu) 4日目の記事です!
VALU 社として記事を書きたい! と名乗りを上げて2年目になりました。ことしもよろしくおねがいします!

今回は「Makefile を利用して iOS 開発を賢く運用しよう」ということで，`Makefile` の補完機能を用いまして，チームメンバーが快適に，かつ本質に集中できる環境を維持することのできる試みのご紹介です。

## 目次

- [Makefile との出会い](https://qiita.com/YutoMizutani/items/6eea2ffac577ff481039#makefile-%E3%81%A8%E3%81%AE%E5%87%BA%E4%BC%9A%E3%81%84)
- [Makefile によるスクリプト管理は何が嬉しいのか](https://qiita.com/YutoMizutani/items/6eea2ffac577ff481039#makefile-%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E7%AE%A1%E7%90%86%E3%81%AF%E4%BD%95%E3%81%8C%E5%AC%89%E3%81%97%E3%81%84%E3%81%AE%E3%81%8B)
- [iOS で実際に利用している Makefile の中身](https://qiita.com/YutoMizutani/items/6eea2ffac577ff481039#ios-%E3%81%A7%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B-makefile-%E3%81%AE%E4%B8%AD%E8%BA%AB)
- [Makefile に置くべき便利なワンライナー](https://qiita.com/YutoMizutani/items/6eea2ffac577ff481039#makefile-%E3%81%AB%E7%BD%AE%E3%81%8F%E3%81%B9%E3%81%8D%E4%BE%BF%E5%88%A9%E3%81%AA%E3%83%AF%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%8A%E3%83%BC)
- [おわりに](https://qiita.com/YutoMizutani/items/6eea2ffac577ff481039#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB)
- [References](https://qiita.com/YutoMizutani/items/6eea2ffac577ff481039#references)

## Makefile との出会い

初めて Makefile と出会ったのは，そうですね。もともとは組み込み寄りであった自身の弱点を克服するため，[gohome](https://github.com/YutoMizutani/gohome) という Go 製の API サーバーを作成していた最中の出来事でした。

Go 言語自体も初めてであったことで色々調べているうちに [Golang を使うなら Makefile を恐れるな](https://frasco.io/golang-dont-afraid-of-makefiles-785f3ec7eb32) という記事に出会いました。

本来の Makefile の使い方とは異なりますが，`Makefile` を作成し，よく使うコマンドを `:` 区切りの Key とスクリプトで構成する。ターミナルから `$ make build` などと入力してスクリプトを実行させると便利だよ，という記事です。

> Makefile の例。画像は [YutoMizutani/gohome/Makefile](https://github.com/YutoMizutani/gohome/blob/master/Makefile) から一部を抜粋
> <img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195913/a2c047fd-2735-b29c-7ecb-50a38da72c16.png">

わたしはこれを読み大変感動しまして，ぜひ iOS にも導入しようと決意しました。

## Makefile によるスクリプト管理は何が嬉しいのか

#### 環境構築を一括で管理できる

Makefile を利用することで，個々のコマンド操作を統制することが可能です。
最も恩恵を得られるのは新しい人が参入した際の環境構築だと思います。

あなたの `README.md` はどうでしょうか?「CocoaPods のインストール方法は...」等の長い文章で書かれていませんか? 利用されているツールしか書かれていない場合もあるかと思います。
新しい開発者の目的は，「そのリポジトリがどう動いているかを知ること」より「目的の機能または問題に着手すること」です。

フルスタックエンジニアが助けに来たとしても，1日を環境構築で終えるような組織ではとてももったいないですよね。

VALU では `$ make all` のみで環境構築が完了します:sparkles::sparkles:

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195913/326c4883-f756-aa2c-806a-55e2b7d538ba.png">

画像のように GitHub から clone した状態から，`make all && make open` を入力するだけで自動で Xcode が起動し，そのまま開発を進めることができます。

#### 補完が効く

[zsh-completions](https://github.com/zsh-users/zsh-completions) などの補完用プラグインを用いることで，どんなコマンドがあるかを実行前に確認することができます。
これが ShellScript だけでなく Makefile を利用する利点です。

<img width="562" alt="screenshot 452.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195913/5356729a-c843-800c-4cfa-20c36e8e283c.png">

#### Branch 間の環境変化にも常に同じコマンドで対応できる

「今回新しく CocoaPods を Homebrew から Gem を利用するように変えました! 以降開発者はこのコマンドを利用してください! 古いコマンドは使わないでください!」

という初心者が泣いてしまう呪文を `Makefile` は解決します。

`Makefile` 内にスクリプトを書いてコマンドとスクリプトを分離させることで変更に強くなります。iOS 風に言うと `protocol` 経由でアクセスする，と言えばイメージがしやすいですかね。

呼ぶ側は常に `make` コマンドから。 `$ make install` と呼ぶことで環境が変わってもインストール作業が継続できます。

#### 自動環境構築の肝，自動インストール

[Homebrew](https://brew.sh/index_ja) がない? インストールします。 [CocoaPods](https://cocoapods.org/) ももちろん。Ruby のバージョンが違う? [rbenv](https://github.com/rbenv/rbenv) を利用するように変更します，というように存在しない場合にも自動でインストールさせるようにしています。

最近「[おま環](https://word-dictionary.jp/posts/3193)」という言葉を知りました。OSS だとそうもいきませんが，会社PCの社内プロジェクトなので，必要なものは自動で構築させる方が効率的です。

以下は Xcode のバージョンを指定するスクリプトです。CocoaPods 等の Ruby 製ツールは，VALU では Gemfile を用いて管理しています。
各スクリプトは，そのコマンドの実行前に `type` `gem contents` `mint list | grep -s` 等を用いて目的のコマンドが存在するかを判定し，存在しない場合にインストールスクリプトが走るようにしています。
`$(dirname $0)/../gems/bundle-install.sh` の先では， `bundle` コマンドが存在するかを判定しており，必要に応じて Ruby 自体がインストールされているかまで遡り，対応が必要なPCにのみ適切にインストールが走るようになっています。

このような綺麗なスクリプトも `$ sh select-xcode-version.sh` と長ければ全メンバーに実行してもらえない可能性があります。`sh` って打てば補完されるだろう，というのは「[おま環](https://word-dictionary.jp/posts/3193)」でした。
`make` にコマンドを集めておき，短いコマンドで呼び出せるようにしておくことでスクリプトとの距離が縮まります! すてきです!

```select-xcode-version.sh
#!/bin/sh

# Install Gems
if !(gem contents xcode-install > /dev/null 2>&1); then
    sh $(dirname $0)/../gems/bundle-install.sh
fi

# Check argv
if [ $# -ne 1 ]; then
    echo "select-xcode-version.sh: error: Required the version" 1>&2
    exit 1
fi

# xcode-install says `Xcode VER.SION.NUM.BER`
XCVERSION_OUTPUT_PREFIX="Xcode "

EXPECT_XCODE_VERSION=$1
CURRENT_XCODE_VERSION=`xcversion selected | grep -i $XCVERSION_OUTPUT_PREFIX | tr -d $XCVERSION_OUTPUT_PREFIX`

# Select Xcode version if these are different
if test $EXPECT_XCODE_VERSION != $CURRENT_XCODE_VERSION; then
    bundle exec xcversion install $EXPECT_XCODE_VERSION ; :
    bundle exec xcversion select $EXPECT_XCODE_VERSION
fi
```

## iOS で実際に利用している Makefile の中身

Swift も LLVM を利用している言語ですが，Xcode によってコンパイル周りのオプションは肩代わりすることができています。
[Swift 自体のビルドに必要なすごくながいコマンド](https://github.com/apple/swift/blob/master/CMakeLists.txt) とは異なり，とてもシンプルに記述することができます。
あくまで `Makefile` を使おうというものなので，おまじないや固有表現を避け，ShellScript が読める人なら内容が読めるような形に抑えています。
加えて，具体的なコマンドは環境に依存するパスおよび拡張子以外は別の ShellScript としてファイルを分けており，CI 時に必要なスクリプトは個々に実行できる環境を作り上げています。

こちらの Makefile は「人間がコマンドを覚えず，内容を知らなくとも実現したいことを実現する」という方針で設計しており，
利用者は「とりあえず `$ make` を打ってみて，補完からデプロイやビルドなどのしたいことを探す」
成長したい人は「Makefile を覗いてみて，必要に応じて質問や修正，追加を行う」
ことで業務の効率化を実現しています。

これにより，QA チームへの環境構築や作業依頼についても「`$ make clean` をしてみてください」と Slack や口頭で伝達することができるようになっています:tada:

```Makefile:Makefile
# Paths
PROJECT_PATH=./

# File extensions
PROJECT_EXTENSION=.xcodeproj
WORKSPACE_EXTENSION=.xcworkspace

# Definition
PROJECT_NAME=`find $(PROJECT_PATH) -maxdepth 1 -mindepth 1 -iname "*$(PROJECT_EXTENSION)" | xargs -L 1 -I {} basename "{}" $(PROJECT_EXTENSION)`
PROJECT=$(PROJECT_PATH)$(PROJECT_NAME)$(PROJECT_EXTENSION)
WORKSPACE=$(PROJECT_PATH)$(PROJECT_NAME)$(WORKSPACE_EXTENSION)
XCODE_VERSION=`cat $(PROJECT_PATH).xcode-version`

# Xcode を開く
open:
	sh scripts/general/xcode/open-xcode.sh $(WORKSPACE)
# Xcode を強制終了させる
kill:
	sh scripts/general/xcode/kill-xcode.sh


# 一括で環境構築を行う
all:
	make config
	make select
	make generate
	make install
	make sort
	make clean

# ~~~ 省略 ~~~

# ビルドに必要なファイルを生成する
generate:
	make generate-xcodeproj
generate-xcodeproj:
	sh scripts/general/xcodegen/generate-xcodeproj.sh

# 依存するツールやライブラリをインストールする
install:
	make install-mint
	make install-gems
	make install-pods
install-mint:
	sh scripts/general/mint/mint-install.sh
install-gems:
	sh scripts/general/gems/bundle-install.sh
install-pods:
	sh scripts/general/cocoapods/pod-install.sh
```

余談ですが，この `$ make all` 内では複雑なスクリプトを隠蔽しています。
上記の `make config` 内では，`post-checkout` の Git hook に紐づけてインストール (bootstrap) を入れています。
発生し得るトラブルには事前に対応しましょう。
「ブランチを変えたら (ライブラリのバージョンが変わったために) ビルドできなくなった:cry:」という苦言に毎回対応するのも，そのためのドキュメントを保守するのもつらいです。

## Makefile に置くべき便利なワンライナー

この程度，`.bashrc` に書いておけという意見も分かります。
一方で，リポジトリで共有するということは「あの人の便利な技」チーム全体がその commit から便利になるということです。
何を書いたら良いか分からないという方へ! まずこれを入れて幸せになりましょう!

#### ディレクトリ配下の Xcode workspace を検索して起動する

`open` コマンドは macOS で default で指定されたアプリを用いて起動させるコマンドです。実際にはプロジェクトディレクトリや `xcworkspace` の拡張子を変数に切り出していますが， `xcworkspace` を利用していることが分かっている場合は，より短いコマンドでターミナルから Xcode を開くことが可能です。

```sh:ターミナル
$ open `find . -maxdepth 1 -mindepth 1 -iname "*.xcworkspace"`
```

`-a` オプションによって指定したアプリを用いて開くことも可能です。複数の Xcode をインストールしている場合，`xcode-select` で指定されている PATH を利用して起動させることもできます。

```sh
PROJECT_PATH=`find . -maxdepth 1 -mindepth 1 -iname "*.xcworkspace"`
XCODE=`xcode-select --print-path | awk 'match($0, /^.*.app/){ print substr($0, RSTART, RLENGTH) }'`
open -a $XCODE $PROJECT_PATH
```

#### キャッシュと Derived Data の削除

よく使うキャッシュと Derived Data の削除を行うワンライナーです。` ; :` と最後に付けることで `rm` 時に Fail しないようにしています。
われわれは記憶のテストをしているわけではないので DerivedData の場所 ( `~/Library/Developer/Xcode/DerivedData` ) は暗記するよりどこかに書いておきましょう。

```sh:ターミナル
$ xcodebuild -alltargets clean ; rm -rf ~/Library/Developer/Xcode/DerivedData/ ; :
```

#### Xcode project をソートする

有名な [Xcode のソートスクリプト](https://github.com/WebKit/webkit/blob/master/Tools/Scripts/sort-Xcode-project-file) をありがたく利用させていただくワンライナーです。Perl ファイルなので `curl` でお借りして使い終わったら破棄するだけです。
`Makefile` 内に書いておくことで，常に綺麗な状態で開発を始めることができて便利です。

```sh:ターミナル
$ curl -sS https://raw.githubusercontent.com/WebKit/webkit/master/Tools/Scripts/sort-Xcode-project-file \
    > ./script.pl \
    && perl ./script.pl `find . -maxdepth 1 -mindepth 1 -iname "*.xcodeproj"` \
    && rm -f ./script.pl
```

## おわりに

これらは別のリポジトリとして GitHub に公開した方が良いのでは?という声もありました (失念しましたが，実際別リポジトリからスクリプトを叩けるものを最近見かけましたね)。
一方で，ブランチを分けることによって考慮しなければならない (e.g. Xcode のバージョン更新時に一部スクリプトは利用できるが，これは巻き戻さないといけない) 等の問題が発生するためにやめました。Makefile およびスクリプトを一人で書いているので......:innocent:

[Alfred](https://www.alfredapp.com/) を利用してあれこれする便利ですが，簡単な操作はターミナルで完結させるのが良いです。ターミナルって結構便利なんですよ，ということが少しでも伝われば幸いです。難しいところは強い人にやってもらって，その恩恵を慣れていない人にも受けられるようにする。みんながハッピーになるといいな。

[VALU Advent Calendar 2019](https://qiita.com/advent-calendar/2019/valu)，明日は弊社 CTO @mito_memel さんのすてきなサーバーサイドのお話です。

## References

- [YutoMizutani/gohome - GitHub](https://github.com/YutoMizutani/gohome)
- [Golang を使うなら Makefile を恐れるな](https://frasco.io/golang-dont-afraid-of-makefiles-785f3ec7eb32)
- [macOS用パッケージマネージャー — Homebrew](https://brew.sh/index_ja)
- [CocoaPods](https://cocoapods.org/)
- [rbenv/rbenv - GitHub](https://github.com/rbenv/rbenv)
- [おま環」とは？意味や使い方を元ネタを含めてご紹介 - コトバの意味辞典](https://word-dictionary.jp/posts/3193)
- [Carthage/Carthage - GitHub](https://github.com/Carthage/Carthage)
- [Alfred - Productivity App for macOS](https://www.alfredapp.com/)
- [zsh-users/zsh-completions](https://github.com/zsh-users/zsh-completions)
- [apple/swift/CMakeLists.txt - GitHub](https://github.com/apple/swift/blob/master/CMakeLists.txt)
- [WebKit/webkit/Tools/Scripts/sort-Xcode-project-file](https://github.com/WebKit/webkit/blob/master/Tools/Scripts/sort-Xcode-project-file)