この記事は、株式会社アトラエ アドベントカレンダー16日目の記事です。
前日の記事は、@tsucchi_13の「安全かつクイックなリリースのために、Git flowを拡張してみた」でした。
本日はエンジニアの @ysk_aonoが担当します。直近の業務では、iOSアプリの開発やデータ分析基盤の整備などを行っています。
Carthageのビルドは...なかなか待たされる
iOSアプリの開発を行う際、3rdパーティーのライブラリ導入時etc.にパッケージマネージャを使うのが普通だと思います。
CocoaPodsやCarthage、Swift Package Managerなどが代表例として挙げられるでしょうか。
今回のテーマのCarthageには、
「導入時のビルドでライブラリのフレームワークを作成するため、開発時のアプリビルドのコンパイル時間が短縮される」
といったメリットが主にあるかと思います。
非常に便利な反面、ここで課題になりえるのが事前ビルドのコスト (時間) です。carthage bootstrap
を気軽に実行すると、それなりの待ち時間を要することも多いかと思います。
特に顕著なのが、BitriseのようなCI as a Serviceでビルドを行う時です。
もちろんどのサービスにもキャッシュ機構は備わっており、少し設定を追記すればライブラリ成果物をキャッシュすることは可能ですが、
- 初回やライブラリ更新 (CIサービス上にキャッシュが無い状態) , Xcodeアップデート時のビルドは、時間が非常にかかる
- そのサービス特有のオペレーションを実行する必要があり、デプロイスクリプト (fastlane) のポータビリティが失われがち
- 例えばBitriseでは、独自のCarthageステップを使う必要がある
といった点をもう少し良くしたいと考えました。
特に1つめに挙げた初回/更新時のビルド時間は辛いところで、
例えばRealmのようなビルドコストの大きなライブラリを利用している場合、CIサービスのビルド時間上限に達してしまい「ぐぬぬ...終わらねぇ...」となることも多いです。
(もちろん札束で殴っていく手段もあるのですが、コスト観点も踏まえてそちらの方法はまだ採用していません)
あとから追記
Carthage経由のフレームワークをバージョン管理に含めるという手段も存在しますが、
Gitリポジトリの容量 (と操作) が明らかに重くなるなどのデメリットも見込まれるため、今回は検討外としています
Rome
現在、iOSアプリの開発をかなり少人数 (私以外に1~2人) で行っています。
ライブラリのアップデートについても私が定期的なタイミングで実行することが多く、利用するライブラリバージョンが開発者の間でズレることはほぼありません。
つまり、例えば自身のマシンからCarthageビルド成果物のキャッシュを作ってそれをCIから利用できると、もう少し幸せな感じになりそうです。それを解決してくれそうなのが Rome でした。
Romeが提供してくれる機能の概要ですが、公式のREADMEに書いている通りです。
例えばAmazon S3を、Carthageでビルドした成果物のキャッシュストアとして利用することができます。
Rome is a tool that allows developers on Apple platforms to use:
- Amazon's S3
- Minio
- Ceph
- other S3 compatible object stores
- or/and a local folder
- your own custom engine
as a shared cache for frameworks built with Carthage.
どうやって使うのか
どうやって使うのか、思い出しながら書きます。
1/ インストール
導入はCocoaPodsかHomebrew経由で行えますが、CocoaPodsを採用しました。
iOSプロジェクトのルートディレクトリで、Pods/Rome/rome
コマンドを使えるようになります。
> Pods/Rome/rome --help
Cache tool for Carthage
Usage: rome COMMAND [--romefile PATH] [-v]
Available options:
-h,--help Show this help text
--version Prints the version information
--romefile PATH The path to the Romefile to use. Defaults to the
"Romefile" in the current directory.
-v Show verbose output
Available commands:
upload Uploads frameworks and dSYMs contained in the local
Carthage/Build/<platform> to S3, according to the
local Cartfile.resolved
download Downloads and unpacks in Carthage/Build/<platform>
frameworks and dSYMs found in S3, according to the
local Cartfile.resolved
list Lists frameworks in the cache and reports cache
misses/hits, according to the local
Cartfile.resolved. Ignores dSYMs.
utils A series of utilities to make life easier. `rome
utils --help` to know more
コマンド名の通りですが、rome upload
でキャッシュのアップロード/更新、rome download
でキャッシュのダウンロードができます。
2/ Romefileの作成
Romeでは、動作の設定をRomefile
にyaml形式で記述していきます。
ファイルの置き場所はCartfile
と同じ階層にする必要があるので、だいたいプロジェクトのルートディレクトリになるかと思います。
設定といっても大した記述量ではなく、「どこにキャッシュするか」, **「リポジトリとキャッシュしたいフレームワーク名の対応(※リポジトリ名とフレームワーク名が異なる場合)」**を書けばとりあえず動作するようになるはずです。
キャッシュ場所を1つ以上セットすることが必須条件となります。
※その他Romefileに記述できるブロックについては、公式READMEに載っています。
cache
の指定
今回のケースでは、Amazon S3をキャッシュのアップロード場所として利用するので、以下のように書きます。
cache:
s3Bucket: ios-library-build-cache
s3Bucket
に指定しているのは、S3に存在するバケット名です。(事前に作成が必要です)
ここにビルド成果物のキャッシュが入っていくことになります。
repositoryMap
の指定
ライブラリの中に複数のフレームワークが含まれる場合・リポジトリがGitHub上にない場合など、
リポジトリ名とフレームワーク名が一致しないケースがしばしば存在するかと思います。
そういった場合、Romeはフレームワークの名前解決を行えません。
そこで使うのがrepositoryMap
です。ライブラリの中でキャッシュしたいフレームワークを名前の中で指定します。
例えばRxSwiftの中のフレームワークの一部をキャッシュしたい場合は、以下のように記述します。
repositoryMap:
- RxSwift:
- name: RxSwift
- name: RxCocoa
- name: RxRelay
3/ S3の認証が通るようにする
Amazon S3とデータのやりとりをするわけなので、認証情報を持った状態でRomeを実行する必要があります。
作成したS3バケットにアクセスできるIAMを用意し、おなじみのアクセスキーとシークレットキーを取得します。
4/ キャッシュのアップロード
これで、Macでcarthage bootstrap
して作成したCarthageのビルド成果物を、S3にアップロードできるようになります。
環境変数でAWSの認証情報とバケットの置かれたリージョンを指定し、以下を実行するとS3へのアップロードが開始されます。
AWS_REGION=ap-northeast-1 \
AWS_ACCESS_KEY_ID=XXX \
AWS_SECRET_ACCESS_KEY=XXX \
Pods/Rome/rome upload \
--platform iOS \
--cache-prefix `xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"`
今回はiOSアプリを想定しているので--platform
にはiOSを、
--cache-prefix
には、キャッシュを保存する際のプレフィックス(バケット内でのディレクトリ名)を、指定します。
ちなみに--cache-prefix
にセットしているコマンドですが、実行すると以下のような結果が得られます。
Swiftのバージョン番号をキャッシュのキー名にし、同じXcode環境のCIで後から取り出す時に利用できるようにしています。
> xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"
swiftlang-1200.0.41-clang-1200.0.32.8
Rxswiftであれば、「作成したバケット > swiftlang-1200... > RxSwift」下にこんなファイルとディレクトリができ、
さらにその下にはフレームワークをzipで固めたやつが置いてあります。
手元でキャッシュを更新する場合は、
carthage update
とセットでrome upload
を実行するシェルスクリプトを作っておくといいかもしれませんね。
5/ キャッシュのダウンロード
先ほどと逆の操作です。
CI上や他の開発マシンからキャッシュをDLしたいときは以下を実行すると、キャッシュされたフレームワークがプロジェクトのCarthage/Build
下に入っていきます。
AWS_REGION=ap-northeast-1 \
AWS_ACCESS_KEY_ID=XXX \
AWS_SECRET_ACCESS_KEY=XXX \
Pods/Rome/rome download \
--platform iOS \
--cache-prefix `xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"`
これをCI上で実行できれば、
Carthageのビルド時間がS3からのDL時間に置き換わり、良い感じになりそうです!
CIサービス上でのキャッシュダウンロード
CIサービス(Bitrise)でのアプリビルドには現在fastlaneのスクリプトを利用しており、こちらでRomeのキャッシュダウンロードを行います。
以下がその一部になります。
先述したRomeの実行を、アプリのビルド前にやっているだけです。
これで、Xcodeのバージョンが合ってさえいればCarthageのビルドを行う必要がなくなりました!!!!1
platform :ios do
desc 'Upload a new AdHoc build'
lane :adhoc do
Dir.chdir(ENV['PWD']) do
set_up_carthage
end
...
build_ios_app
...
end
end
desc 'Set up carthage'
private_lane :set_up_carthage do
sh("Pods/Rome/rome download --platform iOS --cache-prefix #{rome_cache_prefix} --concurrently")
end
def rome_cache_prefix
sh("xcrun swift --version | head -1 | sed 's/.*\\((.*)\\).*/\\1/' | tr -d \"()\" | tr \" \" \"-\"").chomp
end
end
※AWSの認証情報などは、環境変数としてCIサービスに登録が必要です。
※ENV['PWD']
には、Bitrise上でアプリのソースコードが置かれている/Users/vagrant/git
を指定しています。
蛇足 : ライブラリのアップデートも自動でやる ※あまりうまくいっていない
ライブラリのアップデートもCIのワークフローとしてやりたいなと思い、Romeを絡めてfastlaneのコードを書いてみました。
platform :ios do
desc 'Update libraries and create PR'
lane :update_libraries do
Dir.chdir(ENV['PWD'])
set_up_carthage
sh('carthage', 'update', '--platform', 'iOS', '--cache-builds', '--no-use-binaries')
sh("Pods/Rome/rome upload --platform iOS --cache-prefix ci-update-#{rome_cache_prefix} --concurrently")
# GitHubでPullRequestを作成
# ...
end
desc 'Upload updated carthage build to S3'
lane :upload_updated_carthage_build do
Dir.chdir(ENV['PWD'])
sh("Pods/Rome/rome download --platform iOS --cache-prefix ci-update-#{rome_cache_prefix} --concurrently")
sh("Pods/Rome/rome list --missing --platform iOS --cache-prefix #{rome_cache_prefix} | awk '{print $1}' | xargs -I {} rome upload \"{}\" --platform iOS --cache-prefix #{rome_cache_prefix} --concurrently")
end
end
上記の2つのワークフローはざっくり下記のようなことを行っており、
組み合わせて使えば定期的なライブラリ更新を実現できそうです。
-
carthage update
→ 普段とは異なるプレフィックス (ci-update-xxx) をつけてキャッシュ保存 → GitHubでライブラリ更新のPR作成 - ci-update-xxxプレフィックス付きのキャッシュを取得 → 通常プレフィックスのキャッシュ上でヒットしなかったものを保存し直す
しかし、carthage update
の実行時間が長くビルド上限に引っかかることが多いため、
現状だとうまく機能しているとはいえません
おわりに
Romeのおかげで、Carthageのビルド成果物をお手軽にキャッシュできるようなりました。
Carthageがもたらしてくれる開発の快適さ、継続的なビルドや配信の速度、fastlaneスクリプトのポータビリティを担保することができました。
※最近CarthageをSwiftPMに移行することを検討して試行錯誤し、まだうまくいっていないためにCarthageをそのまま利用していることは黙っておきます。 Carthageもありがとう!!
以上になります。
明日は、フロントエンドエンジニアの @kato5315が書きます!
PR
Greenやwevox, yentaといったサービスを提供する弊社アトラエですが、
現在、世界中を魅了するプロダクトを作りたいエンジニアを大募集しています!!