LoginSignup
78
47

iOSアプリ開発が捗るMakefileのタスク一覧(開発・CI)

Last updated at Posted at 2020-07-23

はじめに

iOSアプリの開発とCIに使える Makefile のタスク一覧を紹介します。

CDに使えるタスクも載せたかったのですが、文量が多くなったのでいずれ別記事で紹介したいです。

「Makefile」とは?

make コマンドで使われるファイルです。
make は、本来はC言語などのコンパイルやリンクに使われるコマンドです。

しかし、 Makefile に定義したタスク(本来は「ターゲット」と呼ばれるが、本記事では便宜上「タスク」と呼ぶ)を make {タスク名} として実行できるため、タスクランナーとしてもよく使われます。
Node.jsのnpmを使ったことがある方は、 package.jsonscripts と同じ役割と考えるとわかりやすいです。

iOSアプリ開発には標準でタスクランナーを定義する仕組みがないため、 Makefile を使うことにします。

Makefileとシェルスクリプトの違い

Makefile のタスクは、シェルのコマンドを1行ずつ実行します。
シェルスクリプトでも実現できることが多いので、メリットとデメリットを紹介します。

Makefileのメリット

1ファイルで済む

シェルスクリプトは1ファイル1タスクですが(違ったらすみません)、 Makefile は1ファイルに複数のタスクを定義できます。
ファイルが散らばらず、名前も Makefile 固定なので、タスクをどこに定義したか忘れづらいです。

広く知られている

iOSアプリ開発ではあまり使われていないかもしれませんが、他の言語では昔からよく使われています。
Wikipediaによると、 make コマンドの初版は1977年です。

そのため、リポジトリ内に Makefile があると、知っている人なら「ここにあるタスクを実行すればビルドなどができるんだな」とわかります。

補完が効く

bashなどのシェルで設定すれば補完が効くため、頻繁に実行する処理は Makefile のタスクとして定義するとかんたんに何度も実行できます。

補完の設定方法は以下の記事をご参考にしてください。
Homebrewでインストールできるオススメパッケージ - Qiita

Makefileのデメリット

複雑な処理が実行しづらい

私もほとんど理解していないのですが、 Makefile の仕様は複雑なので、難しいことをやろうとするとキャッチアップに時間がかかります。
タスクランナーとして使う場合、単純な処理のみ実行するのがいいと思います。
もし複雑な処理を実行したい場合、タスクからシェルスクリプトを呼び出すのがいいかもしれません。

本記事で説明しないこと

makeコマンドやMakefileの詳細な仕様

そもそも私が最低限の知識しかないので説明できませんw

各ライブラリ管理ツールやライブラリの概要や使い方

私が以前書いた記事を参考にしてください。

環境

  • make:GNU Make 3.81
  • Xcode:11.6 (11E708)
  • Swift:5.2.4

Makefileのタスク一覧

私がiOSアプリの開発とCIで定義しているタスクをひとつずつ紹介します。

setup

ライブラリのインストールなど、開発環境を構築します。

私はできる限り make setup をローカルで実行するだけで、ビルドできる状態まで開発環境が完成するようにしています。
そのため、基本的にはリポジトリをクローンした最初の1回のみ実行します。

Makefile
.PHONY: setup
setup: # Install dependencies and prepared development configuration
	$(MAKE) install-ruby
	$(MAKE) install-bundler
	$(MAKE) install-templates
	$(MAKE) download-firebase-sdk
	$(MAKE) install-mint
	$(MAKE) install-carthage
	$(MAKE) generate-licenses

$(MAKE) {タスク名} で他のタスクを呼び出しています。
これらのタスクについては後述します。

# {コメント} でコメントが書けるので、タスクの概要を記述しています。

よくREADMEに開発環境の構築手順を記載しますが、 make setup があると手順書がスッキリします。
スクリーンショット 2020-07-23 21.07.29.png

参考:https://github.com/uhooi/UhooiPicBook#readme

install-ruby

.ruby-version に記述されているバージョンのRubyをインストールします。

make setup に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。

Makefile
.PHONY: install-ruby
install-ruby:
	cat .ruby-version | xargs rbenv install --skip-existing

install-bundler

Bundlerで管理しているライブラリをインストールします。

こちらも make setup に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。

Makefile
.PHONY: install-bundler
install-bundler: # Install Bundler dependencies
	bundle config path vendor/bundle
	bundle install --jobs 4 --retry 3

CI時にインストール先のフォルダをキャッシュしたいため、パスを明示的に指定しています。

update-bundler

Bundlerで管理しているライブラリを更新します。

頻繁には呼び出しませんが、コマンドが多少長いので定義しておくと便利です。

Makefile
.PHONY: update-bundler
update-bundler: # Update Bundler dependencies
	bundle config path vendor/bundle
	bundle update --jobs 4 --retry 3

install-mint

Mintで管理しているライブラリをインストールします。

こちらも make setup に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。

Makefile
.PHONY: install-mint
install-mint: # Install Mint dependencies
	mint bootstrap --overwrite y

install-cocoapods

CocoaPodsで管理しているライブラリをインストールし、ワークスペースを生成します。

後述する全体図を追うとわかりますが、 make setup で実行されます。
そのため、こちらも基本的にはライブラリの追加時にしか直接呼び出しません。

Makefile
.PHONY: install-cocoapods
install-cocoapods: # Install CocoaPods dependencies and generate workspace
	bundle exec pod install

私はCocoaPodsをBundlerで管理しているため、先頭に bundle exec を付けて実行しています。
Makefile の作成前はよく付け忘れていたので、短い処理でもタスクとして定義すると忘れません。

update-cocoapods

CocoaPodsで管理しているライブラリを更新し、ワークスペースを生成します。

Makefile
.PHONY: update-cocoapods
update-cocoapods: # Update CocoaPods dependencies and generate workspace
	bundle exec pod update

install-carthage

Carthageで管理しているライブラリをインストールします。

こちらも make setup に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。

Makefile
.PHONY: install-carthage
install-carthage: # Install Carthage dependencies
	mint run carthage carthage bootstrap --platform iOS --cache-builds
	@$(MAKE) show-carthage-dependencies

私はCarthageをMintで管理しているため、先頭に mint run carthage を付けて実行しています。

@$(MAKE) show-carthage-dependencies については後述します。

update-carthage

Carthageで管理しているライブラリを更新します。

Makefile
.PHONY: update-carthage
update-carthage: # Update Carthage dependencies
	mint run carthage carthage update --platform iOS
	@$(MAKE) show-carthage-dependencies

show-carthage-dependencies

Carthageでインストールしたライブラリとそのバージョンを出力します。

Makefile
.PHONY: show-carthage-dependencies
show-carthage-dependencies:
	@echo '*** Resolved dependencies:'
	@cat 'Cartfile.resolved'

直接呼び出すことを想定していないので、コメントを記述していません。

Azure PipelinesにあるCarthageのタスクで使われていて、流用させていただいています。

install-templates

Generambaのテンプレートをインストールします。

make setup に含んでいるため、基本的にはテンプレートの追加や変更時にしか直接呼び出しません。

Makefile
.PHONY: install-templates
install-templates: # Install Generamba templates
	bundle exec generamba template install

download-firebase-sdk

FirebaseのSDKをダウンロードします。

こちらも make setup に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。

Makefile
FIREBASE_VERSION := 8.6.0

.PHONY: download-firebase-sdk
download-firebase-sdk: # Download firebase-ios-sdk
	curl -OL https://github.com/firebase/firebase-ios-sdk/releases/download/${FIREBASE_VERSION}/Firebase.zip
	unzip -o Firebase.zip -d Frameworks/
	rm -f Firebase.zip

SwiftPMなどのパッケージ管理ツールで管理するとビルドが非常に遅くなるため、私は手動でダウンロードして入れています。

リダイレクトが発生するので、 curl コマンドには -L オプションを付ける必要があります。

generate-licenses

LicensePlistを使ってライセンス情報を生成し、プロジェクトを生成し直します。

PRODUCT_NAME はプロジェクトに応じて変更してください。

Makefile
PRODUCT_NAME := UhooiPicBook

.PHONY: generate-licenses
generate-licenses: # Generate licenses with LicensePlist and regenerate project
	mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle --add-version-numbers
	$(MAKE) generate-xcodeproj

$(MAKE) generate-xcodeproj については後述します。

generate-module

Generambaを使ってモジュールを生成し、プロジェクトを生成し直します。

モジュール名を指定する必要があるので、 make generate-module MODULE_NAME=Foo のように呼び出します。

Makefile
MODULE_TEMPLATE_NAME ?= uhooi_viper

.PHONY: generate-module
generate-module: # Generate module with Generamba and regenerate project # MODULE_NAME=[module name]
	bundle exec generamba gen ${MODULE_NAME} ${MODULE_TEMPLATE_NAME}
	$(MAKE) generate-xcodeproj

どのテンプレートを使うか指定する必要がありますが、私は自作のテンプレートのみ使っているので、 Makefile に直接記述しています。

generate-xcodeproj

XcodeGenを使ってプロジェクトを生成し、CocoaPodsで管理しているライブラリをインストールしてワークスペースを作成して、Xcodeで開きます。

Makefile
.PHONY: generate-xcodeproj
generate-xcodeproj: # Generate project with XcodeGen
	mint run xcodegen xcodegen generate
	$(MAKE) install-cocoapods
	$(MAKE) open

Xcode以外でファイルを追加や変更した場合、XcodeGenでプロジェクトを生成し直す必要があるため、 make generate-licensesmake generate-module でも実行しています。

プロジェクトを生成し直した場合、ワークスペースも生成し直す必要があるため(違ったらすみません)、 $(MAKE) install-cocoapods を実行しています。

$(MAKE) open については後述します。

open

ワークスペースをXcodeで開きます。

make open を実行するだけで対象のプロジェクトがXcodeで開くのは地味に便利です。
ターミナルからXcodeへシームレスに移動できます。

Makefile
PRODUCT_NAME := UhooiPicBook
WORKSPACE_NAME := ${PRODUCT_NAME}.xcworkspace

.PHONY: open
open: # Open workspace in Xcode
	open ./${WORKSPACE_NAME}

ワークスペースを使っていない場合、拡張子を .xcodeproj に変えてください。

clean

プロジェクトをクリーンします。

以下で管理しているライブラリのキャッシュと、Generambaのテンプレートを削除します。

  • CocoaPods
  • Carthage
  • Bundler

Xcodeのクリーンも行うようにしています。
たまに失敗してそれ以降のコマンドが実行されないことがあるため、最後に実行するのがいいです。

Makefile
.PHONY: clean
clean: # Delete cache
	rm -rf ./Pods
	rm -rf ./Carthage
	rm -rf ./vendor/bundle
	rm -rf ./Templates
	xcodebuild clean -alltargets

他にも削除できるキャッシュはいろいろあるので、必要に応じて追加や削除してください。

analyze

SwiftLintでアナライズします。

詳細は以下の記事をご参照ください。
SwiftLintのAnalyzeを使って高度な解析をする方法 - Qiita

Makefile
.PHONY: analyze
analyze: # Analyze with SwiftLint
	$(MAKE) build-debug
	mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./${XCODEBUILD_BUILD_LOG_NAME}

build-debug

デバッグビルドします。

Makefile
PRODUCT_NAME := UhooiPicBook
WORKSPACE_NAME := ${PRODUCT_NAME}.xcworkspace
SCHEME_NAME := ${PRODUCT_NAME}

TEST_SDK := iphonesimulator
TEST_CONFIGURATION := Debug

XCODEBUILD_BUILD_LOG_NAME := xcodebuild_build.log

.PHONY: build-debug
build-debug: # Xcode build for debug
	set -o pipefail \
&& xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
build \
| tee ./${XCODEBUILD_BUILD_LOG_NAME} \
| bundle exec xcpretty --color

処理の末尾に \ を付けることで改行できます。改行は任意ですが、可読性のために入れています。

ローカルで実行することは少ないですが、CIでは頻繁に実行します。

xcpretty でログを見やすく整形していますが、生のログより情報量が減るデメリットがあります。
そのため、 tee コマンドで生のログをファイルに出力しています。
CIでは失敗時のみ生のログをアーティファクトにアップロードするのがオススメです。

生のログはGitの管理外にします。

.gitignore
+ xcodebuild_*.log

test

単体テストを実行し、結果をHTMLで出力します。

私はシミュレータのOSと端末にデフォルト値を設定して実行しています。
必要に応じてOSを未指定に変更したり、端末やOSを変数で注入したりしてください。

Makefile
PRODUCT_NAME := UhooiPicBook
WORKSPACE_NAME := ${PRODUCT_NAME}.xcworkspace
SCHEME_NAME := ${PRODUCT_NAME}
UI_TESTS_TARGET_NAME := ${PRODUCT_NAME}UITests

TEST_SDK := iphonesimulator
TEST_CONFIGURATION := Debug
TEST_PLATFORM := iOS Simulator
TEST_DEVICE ?= iPhone 11 Pro Max
TEST_OS ?= 13.6
TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}'

XCODEBUILD_TEST_LOG_NAME := xcodebuild_test.log

.PHONY: test
test: # Xcode test # TEST_DEVICE=[device] TEST_OS=[OS]
	set -o pipefail \
&& xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
-skip-testing:${UI_TESTS_TARGET_NAME} \
clean test \
| tee ./${XCODEBUILD_TEST_LOG_NAME} \
| bundle exec xcpretty --report html --color

UIテストはCIで頻繁に実行したくないので、 -skip-testing オプションでUIテストターゲットを指定してスキップしています。

デバッグビルドと同様、ローカルで実行することは少ないですが、CIでは頻繁に実行します。

get-coverage

Slatherを使ってコードカバレッジをHTMLで出力します。

Makefile
COVERAGE_OUTPUT := html_report

.PHONY: get-coverage
get-coverage: # Get code coverage
	bundle exec slather coverage --html --output-directory ${COVERAGE_OUTPUT}

show-devices

接続されている端末とインストールされているシミュレータの一覧を出力します。

Makefile(Xcode12未満)
.PHONY: show-devices
show-devices: # Show devices
	instruments -s devices

CI時に実行することで、単体テストで指定できるシミュレータがわかるので便利です。

Xcode 12から instruments コマンドが非推奨になり、代わりに xcrun xctrace コマンドを使います。

Makefile(Xcode12以降)
.PHONY: show-devices
show-devices: # Show devices
	xcrun xctrace list devices

Xcode 12で instruments コマンドを実行すると、以下の警告が表示されます。

Xcode12でinstrumentコマンドを実行すると出力される警告
$ instruments -s devices
`instruments` is now deprecated in favor of 'xcrun xctrace' (see `man xctrace` for more information on its replacement)

help

各タスクのヘルプを出力します。

具体的には、各タスクとそのすぐ右に記述したコメントを出力します。
コメントを記述していないタスクは出力されません。

Makefile
.DEFAULT_GOAL := help

.PHONY: help
help:
	@grep -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":[^#]*? #| #"}; {printf "%-42s%s\n", $$1 $$3, $$2}'

.DEFAULT_GOAL に指定することで、単に make と実行したときに make help が実行されます。

試しに今までのタスクを記述した Makefilemake help を実行します。

出力結果
$ make help
setup                                      Install dependencies and prepared development configuration
install-bundler                            Install Bundler dependencies
update-bundler                             Update Bundler dependencies
install-mint                               Install Mint dependencies
install-cocoapods                          Install CocoaPods dependencies and generate workspace
update-cocoapods                           Update CocoaPods dependencies and generate workspace
install-carthage                           Install Carthage dependencies
update-carthage                            Update Carthage dependencies
install-templates                          Install Generamba templates
download-firebase-sdk                      Download firebase-ios-sdk
generate-licenses                          Generate licenses with LicensePlist and regenerate project
generate-module MODULE_NAME=[module name]  Generate module with Generamba and regenerate project
generate-xcodeproj                         Generate project with XcodeGen
open                                       Open workspace in Xcode
clean                                      Delete cache
analyze                                    Analyze with SwiftLint
build-debug                                Xcode build for debug
test TEST_DEVICE=[device] TEST_OS=[OS]     Xcode test
get-coverage                               Get code coverage
show-devices                               Show devices

これで Makefile にどのようなタスクがあるか一覧で出力されるので、タスク名を忘れても Makefile の中身を確認せずに実行できます。

Makefileの全体図

最後に Makefile の全体図を載せます。

変数をまとめて先頭に定義することで、他のプロジェクトへコピペして使い回すときに、先頭だけ修正すればいいので変更漏れが減ります。

Makefile
# Variables

PRODUCT_NAME := UhooiPicBook
WORKSPACE_NAME := ${PRODUCT_NAME}.xcworkspace
SCHEME_NAME := ${PRODUCT_NAME}
UI_TESTS_TARGET_NAME := ${PRODUCT_NAME}UITests

TEST_SDK := iphonesimulator
TEST_CONFIGURATION := Debug
TEST_PLATFORM := iOS Simulator
TEST_DEVICE ?= iPhone 11 Pro Max
TEST_OS ?= 13.6
TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}'
COVERAGE_OUTPUT := html_report

XCODEBUILD_BUILD_LOG_NAME := xcodebuild_build.log
XCODEBUILD_TEST_LOG_NAME := xcodebuild_test.log

MODULE_TEMPLATE_NAME ?= uhooi_viper

FIREBASE_VERSION := 8.6.0

.DEFAULT_GOAL := help

# Targets

.PHONY: help
help:
	@grep -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":[^#]*? #| #"}; {printf "%-42s%s\n", $$1 $$3, $$2}'

.PHONY: setup
setup: # Install dependencies and prepared development configuration
	$(MAKE) install-ruby
	$(MAKE) install-bundler
	$(MAKE) install-templates
	$(MAKE) download-firebase-sdk
	$(MAKE) install-mint
	$(MAKE) install-carthage
	$(MAKE) generate-licenses

.PHONY: install-bundler
install-bundler: # Install Bundler dependencies
	bundle config path vendor/bundle
	bundle install --jobs 4 --retry 3

.PHONY: update-bundler
update-bundler: # Update Bundler dependencies
	bundle config path vendor/bundle
	bundle update --jobs 4 --retry 3

.PHONY: install-mint
install-mint: # Install Mint dependencies
	mint bootstrap --overwrite y

.PHONY: install-cocoapods
install-cocoapods: # Install CocoaPods dependencies and generate workspace
	bundle exec pod install

.PHONY: update-cocoapods
update-cocoapods: # Update CocoaPods dependencies and generate workspace
	bundle exec pod update

.PHONY: install-carthage
install-carthage: # Install Carthage dependencies
	mint run carthage carthage bootstrap --platform iOS --cache-builds
	@$(MAKE) show-carthage-dependencies

.PHONY: update-carthage
update-carthage: # Update Carthage dependencies
	mint run carthage carthage update --platform iOS
	@$(MAKE) show-carthage-dependencies

.PHONY: show-carthage-dependencies
show-carthage-dependencies:
	@echo '*** Resolved dependencies:'
	@cat 'Cartfile.resolved'

.PHONY: install-templates
install-templates: # Install Generamba templates
	bundle exec generamba template install

.PHONY: download-firebase-sdk
download-firebase-sdk: # Download firebase-ios-sdk
	curl -OL https://github.com/firebase/firebase-ios-sdk/releases/download/${FIREBASE_VERSION}/Firebase.zip
	unzip -o Firebase.zip -d Frameworks/
	rm -f Firebase.zip

.PHONY: generate-licenses
generate-licenses: # Generate licenses with LicensePlist and regenerate project
	mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle --add-version-numbers
	$(MAKE) generate-xcodeproj

.PHONY: generate-module
generate-module: # Generate module with Generamba and regenerate project # MODULE_NAME=[module name]
	bundle exec generamba gen ${MODULE_NAME} ${MODULE_TEMPLATE_NAME}
	$(MAKE) generate-xcodeproj

.PHONY: generate-xcodeproj
generate-xcodeproj: # Generate project with XcodeGen
	mint run xcodegen xcodegen generate
	$(MAKE) install-cocoapods
	$(MAKE) open

.PHONY: open
open: # Open workspace in Xcode
	open ./${WORKSPACE_NAME}

.PHONY: clean
clean: # Delete cache
	rm -rf ./Pods
	rm -rf ./Carthage
	rm -rf ./vendor/bundle
	rm -rf ./Templates
	xcodebuild clean -alltargets

.PHONY: analyze
analyze: # Analyze with SwiftLint
	$(MAKE) build-debug
	mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./${XCODEBUILD_BUILD_LOG_NAME}

.PHONY: build-debug
build-debug: # Xcode build for debug
	set -o pipefail \
&& xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
build \
| tee ./${XCODEBUILD_BUILD_LOG_NAME} \
| bundle exec xcpretty --color

.PHONY: test
test: # Xcode test # TEST_DEVICE=[device] TEST_OS=[OS]
	set -o pipefail \
&& xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-workspace ${WORKSPACE_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
-skip-testing:${UI_TESTS_TARGET_NAME} \
clean test \
| tee ./${XCODEBUILD_TEST_LOG_NAME} \
| bundle exec xcpretty --report html --color

.PHONY: get-coverage
get-coverage: # Get code coverage
	bundle exec slather coverage --html --output-directory ${COVERAGE_OUTPUT}

.PHONY: show-devices
show-devices: # Show devices
	instruments -s devices

おわりに

ここまで Makefile を作り込むと、Fastlaneを使わなくてもiOSアプリのCIを実行できます。
CIサービスを移行する場合にも、 Makefile があればコストを減らせます。

そして何より、iOSアプリのCI環境を構築していない方の参考になると嬉しいです。

参考リンク

78
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
78
47