バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

iOSアプリを誰でも簡単にビルドできるようにする

こちらはバイセルテクノロジーズ Advent Calendar 2022の13日目の記事です。前日の記事は畑さんの「Ruby on Railsで発生していたN+1を解消してパフォーマンスを改善した話」でした。

株式会社バイセルテクノロジーズの開発1部でiOSエンジニアとして所属している菅原です。

バイセルは買取・販売のリユース事業に必要な各業務ごとに社内専用のiOSアプリを用意しています。ただし、社内エンジニアの構成比としてはサーバサイド、Webのフロンドエンドのエンジニアが大半を占めており、その中で動作確認のためにiOSアプリをビルドして動かしてもらう必要があります。そこで問題となるのがiOSアプリをビルドするための環境構築のハードルの高さで、ただ動作確認したいだけなのに環境構築に時間が取られてしまい開発効率が下がってしまうことがあります。

本稿ではiOSエンジニア以外の人でも手軽にアプリをビルドしてもらうための環境整備の指針と方法についてご紹介いたします。

目次

対象読者

  • 新しいiOSエンジニアがジョインした際に開発環境の構築で失敗したり質問を受けた経験がある
  • チーム開発でiOSエンジニア以外の人にもXcodeを使ってビルドしてもらう必要がある

背景

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

ゴール

  • 可能な限りiOSアプリをビルドするための前提知識を減らす
  • 人的な操作ミスが入りづらい構造で環境構築する
  • Apple Silicon(M1など)とIntel環境のCPUアーキテクチャが異なる環境でも構築可能にする
  • 環境構築をするために必要なツールのインストール状況やバージョンに差異が出ないようにする(環境依存を排除する)

環境構築の手順書

とある社内アプリを一例にしてみます。このアプリは2019年4月から開発をしており、2020年4月にバージョン1.0.0をローンチしました。 では、当時のバージョン1.0.0のREADMEに書いてあった手順書をご覧ください

手順

※ アプリ名などの固有名詞は抽象的な名前に変えています

1. $ git clone git@github.com:buysell-technologies/App.git
2. $ cd App
    - 作業ディレクトリは `App.xcodeproj` があるところです。
3. $ carthage checkout --use-ssh
    - `--use-ssh` は、プライベートリポジトリを含むため必要です。
4. $ (cd Carthage/Checkouts/ReactorKit && swift package generate-xcodeproj)
5. $ (cd Carthage/Checkouts/Then && swift package generate-xcodeproj)
6. $ rm -f ./Carthage/Checkouts/URLNavigator/Package.resolved
7. $ (cd Carthage/Checkouts/URLNavigator && swift package generate-xcodeproj)
8. $ (cd Carthage/Checkouts/api_client && swift package generate-xcodeproj)
9. $ carthage build --platform iOS --no-use-binaries
10. $ pod install
11. $ open App.xcworkspace
    - `App.xcodeproj` ではないことに注意してください。

どうですか?地獄のような手順書でしょう(笑) これはiOSエンジニアだと見慣れたツールが多いのでなんとか理解は可能なのですが、普段iOS開発をしたことがない人にとっては手順書通りに単純にコマンドを実行していくとしてもかなり厳しいものがあります。また、途中でコマンドが失敗した時の問い合わせも結構多くて、環境構築のトラブルシューティングのためサポートも必要になってしまい、僕自身も辛いことが多くなっていました。

それでは2022年12月現在の最新の手順書をご覧ください。

手順

1. $ git clone --recursive git@github.com:buysell-technologies/App.git
    - git submoduleを含むため `--recursive` をつけます。
2. $ cd App
3. $ make
    - すべてのコマンドが成功するとXcodeプロジェクトが開かれますが、初回は依存ライブラリのダウンロードとビルドが走るため時間がかかります。

とても平和になりました。 make コマンド(詳しくは後述で説明)で羅列していた各コマンドを一括で実行するようにしています。後述に詳しく書いてある通り単純に移行する以外にも工夫をしていますが、少なくとも手順書に環境構築のコマンドを羅列させるのは避けた方が環境構築のハードルは下げることができると思います。まず改善の第一歩としてコマンドを羅列させた手順書をmakeコマンドに移行するのをお薦めします。

環境構築のハードルを下げる

方針としては以下を目指します。

  • 各ツールをプロジェクトごとに独立した形でインストールする
  • チームメンバー全員で各ツールのバージョンを機械的に合わせる
  • makeコマンドを活用して、各ツールのインストールと実行を簡略化する

本稿のサンプルコード

ここからiOS特有の話が増えてきますので、全体を把握するためのサンプルコードが https://github.com/yusuga/xcode-setup になります。今後も順次改善予定ですので本稿の内容と異なる箇所が発生する可能性はありますが、ご了承ください。

本稿で取り扱うツール

最近のiOS開発でよく使うツールです。

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

Xcodeのインストール方法

通常XcodeはApp Store版をインストールすることが多いですが、案件ごとに異なるバージョンのXcodeが必要なこともあるため、Xcodeの管理はXcodesAppDownload a release)の使用を推奨します。Xcodes.appを使用すると手軽に複数のXcodeをインストールして切り替えることが可能となります。

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

各ツールのインストール方法は大きく分けて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'

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

Swift Package Managerの登場

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

CocoaPodsやCarthageなどのサードパーティ製のライブラリ管理ツールはXcodeのメジャーバージョンアップで使用できなくなるケースがあるため、最近ではSwiftPMを優先して採用することが多いです。ただし、配布されるライブラリ自体が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 Desktopは大企業(従業員が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の記述

Makefilemake コマンドが書かれたファイルです。全文は 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コマンドを呼び出して、環境構築に必要なコマンドをすべて実行します。実行が成功するとXcode.appでプロジェクトが開かれれば成功です。

注意

  • 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 {} \;

CIへの移植性

これで $ make app を行うだけでどんな環境でもアプリをビルドできるようになりました。つまりこれはそのままCI上(BitriseやCircleCIなど)でも実行可能な形になっていますので、makeコマンドを使用してipaファイル(アプリバイナリ)を生成することも可能になります。

  • より正確にはipaファイルの生成にはXcodeを開く必要がないため $ make app ではなくてmakeコマンドの組み合わせになります。詳しくはサンプルコード内の.circleci/config.ymlfastlane/Fastfileをご参照ください。

まとめ

環境構築方法を提供するときは、以下の2点に気をつけます。

  1. 手順書にコマンドを羅列させる意味を考え、可能ならそれらをMakefileやスクリプトにコマンドをまとめる。
  2. 環境依存に気をつける。特にbrewなどツールのバージョン管理ができないパッケージ管理は避けて、ツールをプロジェクトローカルにインストールしたり、Docker内に閉じた状態で実行して、なるべく個々の端末状況に左右されない方法を選択する。

僕が一番重視しているのは開発者体験(Developer Experience)です。結局のところ僕たちはより良いコードを書くことに注力すべきで、その前段である環境構築で苦労したくありません。特にサーバやWebを開発している人にとってはiOSの開発環境構築に時間が取られてしまうのは本末転倒です。慣れていないプラットフォームでもスムーズに環境構築できる仕組みを用意することが、より良い開発者体験に寄与するので、常に改善を目指していきたいです。

明日のバイセルテクノロジーズ Advent Calendar 2022は高谷さんの「全社員がSQLを書けるようBQの権限やデータソースを整理して運用している話(前編)」です。そちらもぜひ併せて読んでみてください!