Xcode12時代のCarthageで起こった問題とXCFrameworkへの移行

モバイル基盤部のhiragramです。こんにちは。

私たちは、iOS版クックパッドアプリの開発において、CocoaPodsとCarthageを併用して依存ライブラリを管理しています。しかし、Xcode12の時代がやってきて、Carthageによる依存ライブラリのビルドに問題が生じました。この記事では、どのような問題なのか、そしてどのように対処したのかを紹介します。

3行まとめ

  • Xcode12とCarthageの組み合わせで問題が生じた
  • CarthageからSwiftPMへの移行を模索したが、断念した
  • 2021年2月にリリースされたCarthage 0.37.0でXCFrameworkが正式にサポートされたので、それに移行した

Xcode12と、当時のバージョンのCarthageの組み合わせで生じる問題

Carthageは、中央集権的なCocoaPodsとは対照的に、gitリポジトリを直接指定する分散型のパッケージ管理ツールです。リポジトリに含まれるライブラリのプロジェクト設定を使って、iOSアプリに組み込めるフレームワークをビルドします。Mac上で動くiOSシミュレータと実機の両方で使えるようにするため、それぞれのCPUアーキテクチャのためのバイナリを lipo というツールで1つのバイナリにまとめて、XXX.framework というファイルを作ります。lipoで作られた、複数のアーキテクチャ向けのバイナリを含むものはファットバイナリと呼ばれます。

この仕組みはXcode11以前から用いられてきましたが、Xcode12のツールチェインを使って、従来の方法でライブラリをビルドしようとするとエラーが出て失敗するようになりました。これはXcode12になって、iOSシミュレータ用のバイナリが、従来のIntel製CPUのx86_64用バイナリに加えてApple Siliconのarm64用バイナリを含むようになったことに起因します。iPhone実機用とApple Silicon上のシミュレータ用のバイナリがいずれもarm64用で、lipoの制約によりその両方を1つのファットバイナリに含めることができないためです。

Carthage builds fail at xcrun lipo on Xcode 12 beta (3,4,5...) · Issue #3019 · Carthage/Carthage

こちらのissueでは、シミュレータ向けのビルドをするときに EXCLUDED_ARCHS にarm64を指定することで、 "Apple Silicon上で動くiOSシミュレータ" 向けのコードをバイナリに含めないようにしてarm64(実機用)との衝突を防ぐ、というワークアラウンドが紹介されています。Xcodeのbetaが進むうちに根本的な対処法が見つかるだろうと楽観していましたが、Xcode12が正式にリリースされて以降もこのワークアラウンドが必要であるということがわかりました。そのため、クックパッドアプリもこのワークアラウンドを導入して開発/リリースを続ける必要がありました。

しかし、ワークアラウンドはあくまでその場しのぎですので、抜本的な対応として何をどうするべきか検討することにしました。

Swift Package Manager移行の模索

このセクションで書かれているのは2020年12月時点での状況です。

Carthageに囚われずパッケージ管理について改めて見つめ直す良い機会と捉えて、根本対応の手段を検討しました。その中で、XcodeのGUI上で扱うSwift Package Managerが候補にあがりました。サードパーティの依存管理ツールをハイブリッドで運用するよりも、公式が用意したもののほうが色々と都合がよいに違いありません。

Swift Package Manager(以下SwiftPM)は、Appleが提供する依存パッケージの管理ツールです。Xcode11からSwiftPMの機能が統合され、プロジェクトにSwiftPMのパッケージを含めることができるようになりました。Carthageで導入していた外部ライブラリを、SwiftPM経由で入れるように出来ないかを模索しました。

断念

結論から述べると、SwiftPMへの移行を断念しました。私たちの開発スタイルやプロジェクト構成にマッチしない都合があったからです。その中でもクリティカルだった、ユニットテストに関する問題を紹介します。

RxTestを使ったテストがビルドできない

クックパッドアプリではユニットテストの一部でRxTestを利用しており、リモートブランチを更新するごとにCI環境でテストがビルド/実行されます。しかし、RxSwiftをSwiftPMで導入したときに、RxTestを使っているテストコードがビルドできませんでした。シンボルの重複が起きているというエラーが出て、プロジェクトのビルド設定を変えたりアンブレラフレームワークを挟んだりしても解決できませんでした。RxSwiftのissueでも、多くの人がテストコードのビルドやライブラリのリンクなどについて問題を報告していて、SwiftPM側に問題があるという見解が示されています。

We unfortunately can't support SPM properly due to a critical known bug in SPM affecting many multi-target repos: https://bugs.swift.org/browse/SR-12303

- Migrating from Cocoapods to SPM, UITest target "missing required module 'RxCocoaRuntime'" · Issue #2210 · ReactiveX/RxSwift

上述のコメントからリンクされているバグチケットは報告から半年以上たった現時点でもSwiftPM開発メンバーからのリアクションが無く、私が手元で何かちょっと頑張った程度ではどうにもならないだろうと考え、SwiftPMへの移行を断念しました。

Carthage 0.37.0で新しく追加されたXCFrameworkへの移行

SwiftPMへの移行を模索していたのと同じ時期に、CarthageのリポジトリではXCFrameworkをサポートするための開発が進められていました。 XCFrameworkとは、Xcode11からサポートされた、フレームワークを配布するための新しい構造です。従来の形式では、複数のアーキテクチャのためのバイナリを1つのファットバイナリに統合していましたが、XCFrameworkは単体のプラットフォームのためのフレームワークを複数含む形式です。lipoによって1つのファットバイナリに統合する必要が無くなったため、記事の冒頭で述べたarm64のバイナリが衝突してしまう問題を回避できます。

XCFrameworkの詳細については、WWDC19のトークを御覧ください。

Binary Frameworks in Swift - WWDC19

Carthageでは0.37.0から、 --use-xcframeworks というオプションをつけることでXCFrameworkとしてビルドするようになりました。クックパッドアプリのプロジェクトでは0.37.0を使うことにして、Carthageで入れるライブラリはすべてXCFrameworkにすることにしました。

また、XCFrameworkの場合は従来Build Phaseに足す必要があった copy-frameworks が不要になります。もともと copy-frameworksは、AppStoreへサブミットするときにiOSシミュレータ向けのバイナリを含んでいると機械的にリジェクトされてしまうことに対する回避策でしたが、XCFrameworkは内部でプラットフォームが区別されているため、Xcodeの標準のコピーで事足りるようです。

クックパッドアプリではプロジェクトファイルをgit管理せずXcodeGenで生成していますが、XcodeGenでも従来のフレームワークと同じようにXCFrameworkのリンクを記述できます(クックパッドアプリ開発におけるXcodeGenの活用については、こちらの記事をごらんください)。XcodeGenのymlで、carthage というキーでフレームワークを指定していた所を、 framework というキーで、Carthageのビルドディレクトリにあるxcframeworkファイルを指定するだけです。

Build PhaseのLink Binary With Librariesの欄で、このように従来のフレームワークと同じ様にxcframeworkが指定されています(XcodeGenを使わずにプロジェクトファイルを編集する方は、この様にすれば動くはずです😉)。

f:id:hiragram:20210309145211p:plain

lipo時代のワークアラウンドとして行われていた、arm64用バイナリの除外も必要ないので、M1プロセッサのMac上で動くシミュレータでもアプリを動かすことが出来ます。クックパッドアプリはすでにワークアラウンドを削除しXCFrameworkに移行したバージョンが毎週リリースされており、この移行による問題は今の所報告されていません(クックパッドアプリ開発における毎週の自動サブミットについては、こちらの記事をごらんください)。

ただし、XcodeやCarthageにおけるXCFrameworkのサポートがまだ成熟していないからか、キャッシュの有無の判定が正しくないことがあります。例えばCarthage側でライブラリを更新してバージョンが変わったとき、XcodeやCarthageのキャッシュを一度クリーンしないと新しいXCFrameworkを使ってくれないことが多いです。原因はまだ調べられていませんが、おおよそ以下のことをやると正しくアプリがビルドできるようになります。

  • Xcodeを再起動する
  • Xcodeで Clean Build Folder をする
  • Carthage/Build を削除して再ビルドする

まとめ

iOS版クックパッドアプリでは、Carthageで管理されていたライブラリをXCFrameworkとして扱うようになりました。また、その過程で、SwiftPMなどの別のアプローチについても検証するよい機会となりました。

ちなみに、Xcode12.5 Beta2から、Xcode上のSwiftPMで外部ライブラリをDynamic Frameworkとしてビルドできるようになりました。これによって、先述のRxTestの問題が解決されているかもしれません(まだ未検証です😄)。モバイル基盤部では今後もビルド環境をモダンに保つための取り組みを続けていきます。このような領域に興味がある方は、ぜひ以下のリンクからご応募ください!

info.cookpad.com

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/