📘

グローバル環境を汚染しないXcodeの開発環境構築(ゆめみ大技林 '22 ver)

2022/09/06に公開

本記事は2022年9月に開催された技術書展 13にて無料で配布していたゆめみ大技林 '22の寄稿です。
過去3回に渡って書いていた記事をまとめた内容になります。

グローバル環境を汚染しないXcodeの開発環境構築

昨今のiOSアプリ開発はXcode以外にも必要なツールが増えてきています。例えばオープンソースで配布されているライブラリを使用する場合にはCocoaPodsやCarthageを使い、ソースコードのインデントや書き方のスタイルを修正する場合にはSwiftLintを使うなど、アプリ開発を便利にするための周辺ツールが増えてきました。このようにXcodeのみでアプリ開発ができていた時代から、いくつかのツールを組み合わせていく開発スタイルに変化してきました。

そこで問題となるのが複数人でチーム開発を行う場合です。各開発者のMacに同じバージョンのツールをインストールしないと、ツールのバージョンの差異によってビルドが失敗したり、一部挙動が異なってしまうこともあるため、何らかの方法でチームメンバー全員が同じバージョンのツールを使用するようにしないといけません。また、1人で開発を行なっている場合でも、複数のアプリ開発を行なっていると同様の問題が発生する場合があります。

本稿ではそれらの周辺ツールをチームメンバー全員が機械的に同一のバージョンを使用されるように管理し、異なるアプリ間にも影響しない方法をご紹介します。

本稿で紹介すること

  • 各ツールをプロジェクトごとに独立した形でインストールする方法(本稿ではそれをグローバル環境を汚染しないという表現を用いています)
  • チームメンバー全員で各ツールのバージョンを機械的に合わせる方法
  • makeコマンドを活用して、各ツールのインストールと実行を簡略化する方法

本稿のサンプルコード

本稿のサンプルコードは https://github.com/yusuga/xcode-setup になります。今後も順次改善予定ですので本稿と内容が異なる箇所が発生する可能性はありますが、ご了承ください。

本稿で取り扱うツール

ツール名 用途
CocoaPods iOSアプリ内で使用するライブラリ管理ツール
Carthage 同上
SwiftLint Swiftのコーディングスタイルを機械的に合わせるツール
XcodeGen Xcodeプロジェクトをymlファイルから生成するツール
SwiftGen jpg/png, storyboard, Localizable.stringsなどのリソースファイルへの安全なアクセスを提供するツール

各ツールのドキュメントに書いてあるインストール方法

各ツールのインストール方法は大きく分けて2パターンがあり、それらはいくつかの問題点があります。

1. brew経由でのツールのインストールとその問題点

brewはmacOSのパッケージマネージャーです。いわば、ツールを管理するためのツールです。

まず、brew自体は以下のようにインストールします(執筆時点で公式サイトに記載のもののため今後変更される可能性があります)。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

brew経由でCarthageをインストールする方法です。

$ brew install carthage

brewのメリットは、多くのツールがbrewでの配布をサポートしているため、ツールのインストールが非常に簡単です。デメリットは、brewは1つのツールにつき1つのバージョンしかインストールできません。また、Apple Silicon(M1など)とIntel CPUのMacでは、brewのツールのインストール先が異なるため、ツール実行時のPATHを適切に設定していないとツールの実行に失敗する場合があります。

2. Ruby経由でのツールのインストールとその問題点

CocoaPodsなどRuby製のツールはRubyの gem を使用します。Rubyのメリットは、RubyはmacOSに標準でインストールされているため、すぐ利用可能な点です。

$ sudo gem install cocoapods

上記をさらに工夫して GemfileGemfile.lock を使用することによって、gemでインストールしたいツールのバージョン管理も可能になります。

  • Rubyのことなので詳細は割愛させていただきますが、GemfileからはRubyの bundler を使用して各ツールをインストールし、インストール先も指定可能です。

Gemfileの記載例は以下です。

source 'https://rubygems.org'
gem 'cocoapods', '~> 1.11.0'

Gemfile と Gemfile.lock を使用すれば、本稿の目的であるプロジェクトごとに独立した形でのツールのインストールとバージョン管理ができますが、ここで新たな問題になるのは実行するRuby自体のバージョンもチームメンバーと合わせる必要がある点です。Rubyのバージョンを合わせるためには、 rbenv というツールを使用することで複数のRubyをインストールして切り替えることができるのですが、iOS開発者に rbenv を使ったRubyのバージョン管理方法を覚えてもらう必要がある点はデメリットに感じるため、可能であれば避けたいです。

Swift Package Managerの登場

Swift Package Manager(以下、SwiftPMと記載)はApple純正のライブラリ管理ツールです。XcodeのCommand Line Toolsに含まれているため、Xcodeをインストール後の初回起動時にインストールしていれば使用可能になります。

SwiftPMの登場により、CocoaPodsやCarthageを代替可能になったのですが、配布されるライブラリ側がSwiftPMに対応していないといけないため、残念ながらまだCocoaPodsやCarthageとの併用が続きそうです。

一般的にはSwiftPMはCocoaPodsやCarthageを代替として扱われることが多いのですが、本稿では環境構築を行うためのツールのインストールで使用します。

プロジェクトごとに独立してツールをインストールする方法

Xcodeプロジェクトを含む最終的なディレクトリ構成は以下です。Xcodeのプロジェクト名は App です。

.
├── App
│   ├── Assets
│   └── Classes
├── App.xcodeproj
├── App.xcworkspace
├── Cartfile
├── Cartfile.resolved
├── Carthage
├── Makefile
├── Mintfile
├── Podfile
├── Podfile.lock
├── Pods
├── SwiftPackages
│   ├── Package.resolved
│   └── Package.swift
├── project.yml
└── swiftgen.yml

brewの代替にMintを使用する

MintはSwift製のツールを管理できます。前述の使用ツールのうち、実はCocoaPods以外はSwiftで書かれたツールなため、すべてMint経由でインストールが可能です。

Mintのインストール

Mintの公式ドキュメントでは $ brew install mint のようにbrewを使ったインストール方法が案内されていますが、これをSwiftPMで行います。
まず、Mintをインストールするために以下のようにPackage.swiftを SwiftPackages 配下に記述します(本稿では管理のしやすさの都合上、SwiftPackagesディレクトリを作っていますが必須ではないです)。

// swift-tools-version:5.5
import PackageDescription

let package = Package(
  name: "Dependencies",
  products: [],
  dependencies: [
    .package(
      url: "https://github.com/yonaskolb/Mint.git", 
      .exact("0.17.0")
    ),
  ],
  targets: []
)

次に以下を実行することによって、SwiftPM経由でMintをビルドして、Mintの実行可能バイナリを生成することができます。

$ swift run --package-path SwiftPackages mint

Mintfileの記述

Mintfileには、インストールしたいツールとそのバージョンを記述します。Mintでインストール可能かは、そのツールがSwiftPMに対応しているかどうかで判断できます。ライブラリがGithubにある場合は、 yonaskolb/xcodegen のように Githubのユーザ名/リポジトリ名 と記述します。リポジトリ名に続いて @バージョン と記述するとバージョンを指定できます。

carthage/carthage@0.38.0
yonaskolb/xcodegen@2.25.0
swiftgen/swiftgen@6.6.2
realm/swiftlint@0.49.0

Mint経由で各ツールをプロジェクト内にインストールする

$ mint bootstrap を実行するとMintfileに記述したツールがビルドされて、各ツールの実行可能バイナリを生成することができます。ここで注意点として、デフォルト設定だとツールのインストール先がグローバルな領域になっているため、環境変数の MINT_PATH でインストール先をプロジェクト内になるよう指定します。

以下でSwiftPM経由でインストールしたMintを使ってプロジェクト内に mint bootstrap することができます。

$ MINT_PATH=.mint/lib swift run \
    --package-path SwiftPackages \
    mint bootstrap

Rubyの代替にDockerを使用する

Dockerとは、コンテナ型の仮想環境を実行するためのプラットフォームです。仮想環境とは、macOS上に別のOSを仮想的に構築して、その仮想環境内に閉じた形で色々なアプリケーションを実行できるというイメージです。仮想環境上にRubyをインストールすることによって、macOS上にすでにインストールされているRubyとは独立した形で好きなバージョンのRubyをインストールすることができます。

Dockerのインストール

公式の「Install Docker Desktop on Mac」からインストールしてください。

注意

  • Dockerは大企業(従業員が250人以上、または年間収益が1,000万ドル以上)が商用利用する場合には有料のサブスクリプションが必要なことにご注意ください。

Docker上でCocoaPodsを動かす

Docker上でCocoaPodsを動かすためのDocker Image(CocoaPodsを動作させるために必要なメタ情報等)は、すでにDcoker Hubで配布されているため、Docker Desktop for Macを起動した後に以下のコマンドを実行するだけで、 pod install が実行できます。

docker run \
	--rm \
	-v $(pwd):/local \
	-w /local \
	renovate/cocoapods:1.11.3 \
		pod install

Makeコマンドをタスクランナーとして使用する

ここまででMintとDockerを使った環境構築方法をご紹介しましたが、これらのコマンドを簡単に呼び出すためにMakeコマンドを使用する方法をご紹介します。

Makeコマンドとは

make コマンドは、UNIX系のOSでデフォルトで使用可能なコマンドで、本来はCで書かれたソースコード一式をビルドするためなどに使われていました。本稿ではmakeコマンドをタスクランナー(各コマンドを実行)として使用します。その他の選択肢としてはシェルスクリプトをタスクランナーとして使用する方法もありますが、筆者は両方運用してみた結果、Makeコマンドの記述の方がわかりやすいという理由でMakeコマンドを選択しています。

Makefileの記述

全文は https://github.com/yusuga/xcode-setup/blob/master/App/Makefile をご参照ください。

変数を定義

Makefile内では様々なPATHにアクセスしているため、細かく定義しています。変数名を大文字にしてるのはMakefileの慣例です。定義した変数は $(変数) で展開でき、定義した値が文字列として差し代わるだけの単純な仕組みです。

PRODUCT_NAME := App
SCHEME_NAME := $(PRODUCT_NAME)

# Makefileがあるディレクトリを取得するScript
MAKEFILE_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
MAKEFILE_PATH := $(MAKEFILE_DIR)/Makefile
MINTFILE_PATH := $(MAKEFILE_DIR)/Mintfile
MINT_DIR := $(MAKEFILE_DIR)/.mint
MINT_LIBRARY_DIR := $(MINT_DIR)/lib
SWIFT_PACKAGES_PATH := $(MAKEFILE_DIR)/SwiftPackages
SWIFT_PACKAGES_BUILD_PATH := $(SWIFT_PACKAGES_PATH)/.build
MINT_EXECUTABLE := xcrun -sdk macosx swift run --package-path $(SWIFT_PACKAGES_PATH) mint
XCODEPROJ_PATH := $(MAKEFILE_DIR)/$(PRODUCT_NAME).xcodeproj
XCWORKSPACE_PATH := $(MAKEFILE_DIR)/$(PRODUCT_NAME).xcworkspace
USE_CACHE := false

makefile内でのみ実行するコマンドを変数に定義

複数回使用するかつ、重複して書くと冗長になるコマンドも変数を定義します。変数名を小文字にしてるのは個人的な好みで、前述の定数系の変数とコマンドの変数を区別するためです。

補足

  • MakefileはCI実行時など親ディレクトリから呼び出されることを想定して、makefileやmintfileのPATHを明示的に指定しています。
make := make -f $(MAKEFILE_PATH)
mint := MINT_PATH=$(MINT_LIBRARY_DIR) $(MINT_EXECUTABLE)
mint_run := $(mint) run --mintfile $(MINTFILE_PATH)

CocoaPodsのコマンドは以下のように定義します。前述に記載したのは簡略化したもので追加で、UID(ユーザID)やGID(グループID)、 --allow-root を指定をしています。理由は、CI経由で実行する際にパーミッションの問題を解消するためです。これはローカルのmacOS上のDockerで実行しても特に支障はない設定です。

# https://hub.docker.com/r/renovate/cocoapods
COCOAPODS_IMAGE_TAG := 1.11.3
UID := $(shell id -u root)
GID := $(shell id -g root)
pod := docker run \
	--rm \
	-v $(MAKEFILE_DIR):/local \
	-w /local \
	-u $(UID):$(GID) \
	renovate/cocoapods:$(COCOAPODS_IMAGE_TAG) \
		pod --allow-root

.PHONYを定義

Makeコマンドは実行時にmakeコマンド名と同名のファイルやディレクトリがあるとそれらを優先して解釈されます。そこで .PHONY を定義することでmakeコマンドの実行が優先されるようになります。定義場所はどこでもいいのですが、筆者はまとめて書くのが好みなため一箇所に定義しています。

.PHONY: app app_using_cache
.PHONY: mint mint_which mint_run mint_execute
.PHONY: carthage cocoapods 
.PHONY: asset_files xcodeproj spm xcworkspace open 
.PHONY: clean clean_tools clean_app clean_app_caches

環境構築をまとめて行うコマンドを定義

makeコマンドに app を定義します。app 内ではさらにmakeコマンドを呼び出して、環境構築に必要なコマンドをすべて実行します。

注意

  • makeコマンド内の記述はスペースではなく、タブでインデントしないとmakeコマンドとして認識されない仕様があります。そのため $(make) の前はタブになっています。
# defaultは `make` のみを実行した時に呼び出されるmakeコマンドの指定です。
default: app

app:
	$(make) mint
	$(make) carthage
	$(make) asset_files
	$(make) xcodeproj
	$(make) spm
	$(make) cocoapods
	$(make) open

Carthageはキャッシュを使用してbootstrapしたいことがあるため、コマンドの実行とともに USE_CACHE=true のように変数を定義しています。makeコマンドはパラメータを渡すことができないため Key=Value の形で変数を定義する必要があります。前述の変数の定義では USE_CACHE := false を定義しており、これがデフォルト値となります。

app_using_cache:
	$(make) app USE_CACHE=true

make mint

Mint経由で各種ツールをインストールするmakeコマンドです。

mint:
	$(mint) bootstrap --mintfile $(MINTFILE_PATH)

Mintは mint which でツールが存在するか、mint run でツールを実行できるのですが、それらを呼び出ししやすいように以下のmakeコマンドを定義します。例えば、XcodeのビルドフェーズでSwiftLintを呼び出したいときは make mint_run OPTIONS='swiftlint' のように使用します。

変数が定義済みかの確認は、makeの条件文の ifndef を使用しており、これはタブでのインデントを行いません。

mint_which:
	$(make) mint_execute COMMAND='which'

mint_run:
	$(make) mint_execute COMMAND='run'

mint_execute:
ifndef COMMAND
	$(error "COMMAND not found")
endif
ifndef OPTIONS
	$(error "OPTIONS not found")
endif
	$(mint) $(COMMAND) --mintfile $(MINTFILE_PATH) $(OPTIONS)

make carthage

carthage bootstrap を実行して CartfileCartfile.resolved から各ライブラリをインストールするmakeコマンドです。

変数の USE_CACHE の値を確認するためにmakeの条件式の ifeq を使用しています。Carthage経由で依存ライブラリをビルドするのは時間がかかるため --cache-builds を切り替えるために変数を USE_CACHE で分岐させています。

carthage:
ifeq ($(USE_CACHE),true)
	$(mint_run) carthage bootstrap \
		--platform iOS \
		--no-use-binaries \
		--use-xcframeworks \
		--cache-builds
else
	$(mint_run) carthage bootstrap \
		--platform iOS \
		--no-use-binaries \
		--use-xcframeworks
endif

make cocoapods

pod install を実行して PoffilePodfile.lock から各ライブラリをインストールし、 .xcworkspace を生成するmakeコマンドです。

$(pod) は、前述に変数として定義しているためシンプルなmakeコマンドになります。

cocoapods:
	$(pod) install

make asset_files

SwiftGenで switgen.yml からSwiftファイルを生成するmakeコマンドです。

asset_files:
	$(mint_run) swiftgen config run \
		--config $(MAKEFILE_DIR)/swiftgen.yml

make xcodeproj

XcodeGenで project.yml からxcodeprojを生成するmakeコマンドです。

xcodeproj:
	$(mint_run) xcodegen -s $(MAKEFILE_DIR)/project.yml

make spm

SwiftPM経由でiOSの依存ライブラリをインストールするmakeコマンドです。本稿はXcodeGenを使用しているためSwiftPM経由でインストールしたい依存ライブラリの情報は project.ymlに記載しています。

SwiftPMは、Xcodeを開けば各ライブラリのインストールは開始されるのですが、xcodebuild経由で依存を解決した方が高速なのと、CIでキャッシュする時にも役に立ちます。

spm:
	xcodebuild -project $(XCODEPROJ_PATH) \
		-scheme $(SCHEME_NAME) \
		-resolvePackageDependencies

make open

Xcodeプロジェクトを開くmakeコマンドです。。xed.xcodeproj と .xcworkspace の両方が存在するプロジェクトで、.xcworkspace を優先してプロジェクトを開いてくれるコマンドです。

open:
	xed $(MAKEFILE_DIR)

make clean

環境構築時に生成した各種ファイルを削除するmakeコマンドです。makeコマンドではそういう用途には慣例的に clean が命名されているので合わせています。

make clean で環境構築時に生成したすべてのファイルを削除します。これで環境構築前の状態にリセットされます。

clean:
	$(make) clean_tools
	$(make) clean_app
	$(make) clean_app_caches

Mintに関連するファイルを削除するmakeコマンドです。

clean_tools:
	rm -rf $(SWIFT_PACKAGES_BUILD_PATH)
	rm -rf $(MINT_DIR)

Xcodeに関係するファイルを削除するmakeコマンドです。

clean_app:
	# 本稿ではXcodeGenを使って `.xcodeproj` を生成しているため削除
	rm -rf $(XCODEPROJ_PATH)
	rm -rf $(XCWORKSPACE_PATH)
	rm -rf $(MAKEFILE_DIR)/Carthage
	rm -rf $(MAKEFILE_DIR)/Pods

Xcodeプロジェクトをビルドした時に生成される中間生成物はDerivedDataにキャッシュされるのですが、そのキャッシュをアプリ名から特定して削除するmakeコマンドです。

clean_app_caches:
	find $(HOME)/Library/Developer/Xcode/DerivedData \
		-name $(PRODUCT_NAME)"*" \
		-maxdepth 1 \
		-print \
		-type d \
		-exec \
			rm -rf {} \;

今後の展望

今後の展望としては、さらに必要なツールをなくし、最終的には昔ながらのxcodeporjを開くだけでアプリ開発ができるという世界にしたいです。
具体的には以下のような形でツールの利用方法が変わりそうです。

ツール 今後の展望
CocoaPods SwiftPMに対応したライブラリが増えているので時間の問題
Carthage 同上、または、SwiftPMのコマンドプラグイン
XcodeGen SwiftPMのコマンドプラグイン
SwiftGen SwiftPMのコマンド/ビルドツールプラグイン
SwiftLint SwiftPMのビルドツールプラグイン
Mint Mint経由でインストールしているツールが上記の通りすべて代替されればなくせる

SwiftPMのコマンドプラグインとビルドツールプラグインについて

Swift 5.6からSwiftPMがコマンドプラグインとビルドツールプラグインをサポートしました。そのため、将来的にはCarthage, XcodeGen, SwiftGen, SwiftLintはプラグインに差し代えられる可能性が出てきました。

詳しくは本書でも執筆されている宇佐見さんの「Swift Package Managerのプラグイン機能」をご参照ください。

まとめ

本稿では、MintとDockerを使うことでプロジェクトごとに独立した形で環境構築に必要なツールのインストールとバージョンの管理方法と、Makeコマンドによる環境構築に必要なコマンドの呼び出しを簡略化する方法をご紹介しました。iOS開発は年々周辺ツールが充実し便利になっていくのとともに環境構築周りの複雑さが増してきてましたが、SwiftPMのプラグインなどAppleの純正ツールの登場で、またシンプルな世界に戻れそうです。

本来はiOSアプリ開発のみに集中したいのですが、意外とこういう環境構築周りを整備するのも楽しかったりするので困りものですね(笑) 引き続きより良い環境構築方法を模索していきたいと思います。

Discussion