はじめに
iOSアプリの開発とCIに使える Makefile
のタスク一覧を紹介します。
CDに使えるタスクも載せたかったのですが、文量が多くなったのでいずれ別記事で紹介したいです。
「Makefile」とは?
make
コマンドで使われるファイルです。
make
は、本来はC言語などのコンパイルやリンクに使われるコマンドです。
しかし、 Makefile
に定義したタスク(本来は「ターゲット」と呼ばれるが、本記事では便宜上「タスク」と呼ぶ)を make {タスク名}
として実行できるため、タスクランナーとしてもよく使われます。
Node.jsのnpmを使ったことがある方は、 package.json
の scripts
と同じ役割と考えるとわかりやすいです。
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回のみ実行します。
.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
があると手順書がスッキリします。
参考:https://github.com/uhooi/UhooiPicBook#readme
install-ruby
.ruby-version
に記述されているバージョンのRubyをインストールします。
make setup
に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。
.PHONY: install-ruby
install-ruby:
cat .ruby-version | xargs rbenv install --skip-existing
install-bundler
Bundlerで管理しているライブラリをインストールします。
こちらも make setup
に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。
.PHONY: install-bundler
install-bundler: # Install Bundler dependencies
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
CI時にインストール先のフォルダをキャッシュしたいため、パスを明示的に指定しています。
update-bundler
Bundlerで管理しているライブラリを更新します。
頻繁には呼び出しませんが、コマンドが多少長いので定義しておくと便利です。
.PHONY: update-bundler
update-bundler: # Update Bundler dependencies
bundle config path vendor/bundle
bundle update --jobs 4 --retry 3
install-mint
Mintで管理しているライブラリをインストールします。
こちらも make setup
に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。
.PHONY: install-mint
install-mint: # Install Mint dependencies
mint bootstrap --overwrite y
install-cocoapods
CocoaPodsで管理しているライブラリをインストールし、ワークスペースを生成します。
後述する全体図を追うとわかりますが、 make setup
で実行されます。
そのため、こちらも基本的にはライブラリの追加時にしか直接呼び出しません。
.PHONY: install-cocoapods
install-cocoapods: # Install CocoaPods dependencies and generate workspace
bundle exec pod install
私はCocoaPodsをBundlerで管理しているため、先頭に bundle exec
を付けて実行しています。
Makefile
の作成前はよく付け忘れていたので、短い処理でもタスクとして定義すると忘れません。
update-cocoapods
CocoaPodsで管理しているライブラリを更新し、ワークスペースを生成します。
.PHONY: update-cocoapods
update-cocoapods: # Update CocoaPods dependencies and generate workspace
bundle exec pod update
install-carthage
Carthageで管理しているライブラリをインストールします。
こちらも make setup
に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。
.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で管理しているライブラリを更新します。
.PHONY: update-carthage
update-carthage: # Update Carthage dependencies
mint run carthage carthage update --platform iOS
@$(MAKE) show-carthage-dependencies
show-carthage-dependencies
Carthageでインストールしたライブラリとそのバージョンを出力します。
.PHONY: show-carthage-dependencies
show-carthage-dependencies:
@echo '*** Resolved dependencies:'
@cat 'Cartfile.resolved'
直接呼び出すことを想定していないので、コメントを記述していません。
Azure PipelinesにあるCarthageのタスクで使われていて、流用させていただいています。
install-templates
Generambaのテンプレートをインストールします。
make setup
に含んでいるため、基本的にはテンプレートの追加や変更時にしか直接呼び出しません。
.PHONY: install-templates
install-templates: # Install Generamba templates
bundle exec generamba template install
download-firebase-sdk
FirebaseのSDKをダウンロードします。
こちらも make setup
に含んでいるため、基本的にはライブラリの追加時にしか直接呼び出しません。
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
はプロジェクトに応じて変更してください。
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
のように呼び出します。
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で開きます。
.PHONY: generate-xcodeproj
generate-xcodeproj: # Generate project with XcodeGen
mint run xcodegen xcodegen generate
$(MAKE) install-cocoapods
$(MAKE) open
Xcode以外でファイルを追加や変更した場合、XcodeGenでプロジェクトを生成し直す必要があるため、 make generate-licenses
や make generate-module
でも実行しています。
プロジェクトを生成し直した場合、ワークスペースも生成し直す必要があるため(違ったらすみません)、 $(MAKE) install-cocoapods
を実行しています。
$(MAKE) open
については後述します。
open
ワークスペースをXcodeで開きます。
make open
を実行するだけで対象のプロジェクトがXcodeで開くのは地味に便利です。
ターミナルからXcodeへシームレスに移動できます。
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のクリーンも行うようにしています。
たまに失敗してそれ以降のコマンドが実行されないことがあるため、最後に実行するのがいいです。
.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
.PHONY: analyze
analyze: # Analyze with SwiftLint
$(MAKE) build-debug
mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./${XCODEBUILD_BUILD_LOG_NAME}
build-debug
デバッグビルドします。
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の管理外にします。
+ xcodebuild_*.log
test
単体テストを実行し、結果をHTMLで出力します。
私はシミュレータのOSと端末にデフォルト値を設定して実行しています。
必要に応じてOSを未指定に変更したり、端末やOSを変数で注入したりしてください。
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で出力します。
COVERAGE_OUTPUT := html_report
.PHONY: get-coverage
get-coverage: # Get code coverage
bundle exec slather coverage --html --output-directory ${COVERAGE_OUTPUT}
show-devices
接続されている端末とインストールされているシミュレータの一覧を出力します。
.PHONY: show-devices
show-devices: # Show devices
instruments -s devices
CI時に実行することで、単体テストで指定できるシミュレータがわかるので便利です。
Xcode 12から instruments
コマンドが非推奨になり、代わりに xcrun xctrace
コマンドを使います。
.PHONY: show-devices
show-devices: # Show devices
xcrun xctrace list devices
Xcode 12で instruments
コマンドを実行すると、以下の警告が表示されます。
$ instruments -s devices
`instruments` is now deprecated in favor of 'xcrun xctrace' (see `man xctrace` for more information on its replacement)
help
各タスクのヘルプを出力します。
具体的には、各タスクとそのすぐ右に記述したコメントを出力します。
コメントを記述していないタスクは出力されません。
.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
が実行されます。
試しに今までのタスクを記述した Makefile
で make 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
の全体図を載せます。
変数をまとめて先頭に定義することで、他のプロジェクトへコピペして使い回すときに、先頭だけ修正すればいいので変更漏れが減ります。
# 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環境を構築していない方の参考になると嬉しいです。
参考リンク
-
Top (GNU make)
GNU Makeの公式ドキュメント - make - Wikipedia
- Makefileを自己文書化する | POSTD
- Makefile for iOS App development
-
Makefileを利用してiOS開発を賢く便利に運用しよう🎉 - Qiita
私がiOSアプリ開発にMakefile
を導入するきっかけとなった記事です - Makefile in iOS - Speaker Deck
- UhooiPicBook/Makefile at develop · uhooi/UhooiPicBook
- UhooiPicBook/.gitignore at develop · uhooi/UhooiPicBook
- https://twitter.com/the_uhooi/status/1305343192670199809?s=21