CarthageでビルドしたフレームワークをGitにコミットせずに良さげに扱う

f:id:vasilyjp:20180927112700j:plain

iOSチームの@hiragramです。

所属するプロジェクトでは依存管理にCarthageを使っていますが、Carthageの成果物である Carthage/ 以下をコミットするかどうかはよく議論になる話題かと思います。 私はコミットしない派なので、そのメリットを残しつつデメリットをなくすためにやってみたことを紹介します。

メリットとデメリット

コミットしない派のメリット

  • リポジトリが肥大化しない
    • 以前のプロジェクトでは Carthage/ 以下をコミットしていて、リポジトリがめちゃでかくなってcloneにめちゃ時間がかかる感じになっていました。
  • diffがうるさくならない
    • 言わずもがな。
  • Xcodeのバージョンを積極的に上げられる
    • SwiftのABI安定化はまだ先なので、コミットされたバイナリはそれをビルドしたのと同じバージョンのSwiftからしか扱えない。
    • 複数人で開発していると、新しいバージョンのSwiftでコンパイルされたバイナリをmasterへ取り込む時に、全員せーのでXcodeのバージョンを上げなきゃいけなくなる。
      • XcodeやSwiftのアップデートに追随していく精神的な妨げになるのでよくない。

コミットしない派のデメリット

  • cloneしたあとすぐにアプリが使えない
    • これそんなにデメリットに思わない。 make install とか昔やってましたよね…?
  • CIコンテナの上で依存パッケージもビルドするとえらい時間がかかる
  • 異なる依存パッケージをもつブランチ間を移動するたびに再ビルドしないといけない
    • 確かに。
  • 同じ依存パッケージをもつブランチ間での移動では再ビルドは必要ないが、それが必要なのかぱっと分からない
    • 確かに。
  • 異なる依存パッケージをもつブランチ間を移動するたびに再ビルドしないといけない
  • 同じ依存パッケージをもつブランチ間での移動では再ビルドは必要ないが、そもそも必要なのかどうかぱっと分からない

今回はこれを解決する方法を考えてみました。

どのように解決するか

課題解決の要件はこのようにします。

  • ブランチ切り替えによって Cartfile.resolved が変わった時に毎回再ビルドするのは時間がもったいないので、過去にその依存パッケージ郡をビルドしたことがあったら再ビルドはしなくていいようにする
  • 必要な場合は再ビルドしないとアプリのビルドが通らないようにする
    • = 不要な場合はそのままアプリがビルドできる

Cartfile.resolvedCarthage/ 以下のファイルは1対1で対応するはずです。即ち、 Cartfile.resolved の中身ごとに Carthage/ 以下をキャッシュしておけばブランチ切替時の再ビルドが不要になるはずです。

(厳密にはそうではない。後述します)

シンボリックリンクを活用し、以下のようなディレクトリ構成にしました。

$ tree -L 5
.
├── Cartfile
├── Cartfile.resolved
├── Carthage -> CarthageCache/`なんらかの識別子`
├── CarthageCache
│   └── `なんらかの識別子`
│       └── Build
│           └── iOS
│               ├── RxBlocking.framework
│               ├── RxCocoa.framework
│               ├── RxSwift.framework
│               └── RxTest.framework
.
.
.

$ carthage update(or bootstrap)で出来た Carthage/ ディレクトリを CarthageCache/なんらかの識別子/ に移動し、Carthage ディレクトリの代わりにそこへのシンボリックリンクを置きます。

先程「Cartfile.resolvedCarthage/以下は1対1に対応する」と書きましたが、実は正確ではありません。Carthageは --configuration Debug のようにビルド設定を指定することが出来ます。 Cartfile.resolvedの中身だけで識別子を作ってしまうと、その中身がデバッグビルドなのかリリースビルドなのかわからなくなってしまいます。 そこで「なんらかの識別子」は、 Cartfile.resolved のチェックサムにビルド設定(Release or Debug)を付加したものにします。

Xcodeのプロジェクトにフレームワークを追加する時は、このシンボリックリンク経由のパスで追加します(コツがいります。後述します)。

そしてXcodeのBuild Phaseの先頭で、適宜シンボリックリンクの向き先を変えるようにします。

こんな感じでしょうか。

実装と導入

carthage_hack.sh

こんなシェルスクリプトを書きました。

まず、普段使っている $ carthage update$ carthage bootstrap は今後このコマンドで置き換えます。

$ ./carthage_hack.sh [update|bootstrap] [Release|Debug]

このコマンドは、

  • Carthageでビルド
  • できた Carthage/ ディレクトリからフレームワークを CarthageCache/識別子/Build/iOS/ 以下に移動
  • Carthage/ ディレクトリを削除してそこにシンボリックリンクを設置

を行います。

また、

$ ./carthage_hack.sh symlink [Release|Debug]

このコマンドは現在の Cartfile.resolved と引数に渡したビルド設定に該当するディレクトリを向いたシンボリックリンクを張ります。これはXcodeのBuild PhaseのCompile Sourcesの手前にRun script phaseを追加し

./carthage_hack.sh symlink ${CONFIGURATION}

こう呼び出しています。${CONFIGURATION}はXcodeの環境変数でアプリの現在のビルド設定が入っているので、「アプリはリリースビルドなのに依存フレームワークがデバッグビルドのままだった」という事故を防ぎます。

問題

あとはシンボリックリンク経由のパスでフレームワークをプロジェクトに追加するだけですが、ここで困ってしまいました。

FinderからXcodeにファイルをドラッグ&ドロップで追加する時、シンボリックリンク経由のパスではなく実体のパスで追加されてしまいます(Add filesからやっても同じ)。

これではシンボリックリンクの切り替えによる恩恵が受けられません。

苦肉の策感が否めないですが、以下のような方法で回避出来ました。

f:id:vasilyjp:20180508100838p:plain

  • シンボリックリンク CarthageFolder referenceとして プロジェクトに追加する
    • Folder referenceの中のファイルを直接プロジェクト内で使えないようなので、さらにもうひと手間
  • New group without folder で作ったプロジェクト内の別のGroupに Xcodeのファイルツリーから ドラッグ&ドロップで追加
    • Finderを経由しなければシンボリックリンク経由のパスとして追加できる

ちなみに、画像でいうDependenciesグループが空の状態だと正しく追加できますが、既に何か入ってるところにさらにFolder reference越しにファイルを追加しようとするとおかしなファイル移動が起きたりそれを戻そうとするとXcodeが落ちたりしました。 回避するためには毎回Dependenciesを空にする必要がありそうです。今はプロジェクト初期で依存パッケージが少ないのであまり問題になりませんが、多くなってくると大変かもしれません。

結果

要件を満たせたか確認してみます。

まず、デバッグビルドで依存パッケージをビルドします。

$ ./carthage_hack.sh bootstrap Debug
$ tree -L 5
.
├── Cartfile
├── Cartfile.resolved
├── Carthage -> CarthageCache/c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug
├── CarthageCache
│   └── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug
│       └── Build
│           └── iOS
│               ├── RxBlocking.framework
│               ├── RxCocoa.framework
│               ├── RxSwift.framework
│               └── RxTest.framework
.
.
.

Xcodeにフレームワークを追加して、アプリをデバッグビルドしてみます。 通りました。

では、次にアプリをリリースビルドしてみます。

Failed to read file or folder at /Users/hiragram/Development/teikibin-ios/Carthage/Build/iOS/RxSwift.framework
Command /bin/sh failed with exit code 1

RxSwift.framework が見つからないと言われてビルドが失敗しました。 リリースビルドのフレームワークはまだ無いのでこれも正しいです。

リリースビルドで依存パッケージをビルドします。

$ ./carthage_hack bootstrap Release
$ tree -L 5
.
├── Cartfile
├── Cartfile.resolved
├── Carthage -> CarthageCache/c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Release # 向き先がReleaseに変わった
├── CarthageCache
│   ├── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug
│   │   └── Build
│   │       └── iOS
│   │           ├── RxBlocking.framework
│   │           ├── RxCocoa.framework
│   │           ├── RxSwift.framework
│   │           └── RxTest.framework
│   └── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Release # リリースビルドのフレームワークが置かれた
│       └── Build
│           └── iOS
│               ├── RxBlocking.framework
│               ├── RxCocoa.framework
│               ├── RxSwift.framework
│               └── RxTest.framework
.
.
.

アプリをもう一度リリースビルドしてみます。 今度は通りました。

次に、Cartfileで指定しているフレームワークのバージョンを変えて再ビルドしてみます。 すると、Cartfile.resolvedの中身も変わるので別の識別子でシンボリックリンクが作られました。

$ tree -L 5
.
├── Cartfile
├── Cartfile.resolved
├── Carthage -> CarthageCache/5a4a9fb30dcc42dc98d9982e141446e2544b89b1ecac779d665ff76af7466228-Debug # 新しいほうに切り替わった
├── CarthageCache
│   ├── 5a4a9fb30dcc42dc98d9982e141446e2544b89b1ecac779d665ff76af7466228-Debug # 増えた
│   │   └── Build
│   │       └── iOS
│   │           ├── RxBlocking.framework
│   │           ├── RxCocoa.framework
│   │           ├── RxSwift.framework
│   │           └── RxTest.framework
│   ├── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Debug
│   │   └── Build
│   │       └── iOS
│   │           ├── RxBlocking.framework
│   │           ├── RxCocoa.framework
│   │           ├── RxSwift.framework
│   │           └── RxTest.framework
│   └── c0480ba77cbcf45447d7e0ee1e2f25cb92d83e33cf4a38635015c676f91837af-Release
│       └── Build
│           └── iOS
│               ├── RxBlocking.framework
│               ├── RxCocoa.framework
│               ├── RxSwift.framework
│               └── RxTest.framework
.
.
.

これで、例えばフレームワークのバージョンを上げる作業をしているブランチとmasterブランチの間を移動しても、Build Phaseの先頭でシンボリックリンクが切り替わる仕組みによって 一度すればそれ以降再ビルドが不要になりました。 逆に一度もビルドしていないときは、リリースビルドのフレームワークが見つからなかったときと同様にアプリのビルドが失敗するので 間違ったバージョンを使ってしまう心配もありません。

いい感じじゃないでしょうか。

まとめ

この仕組みはまだ導入したばかりなので、まだ遭遇していないシーンで困ることがあるかもしれません。その時はまた何か考えます。

この仕組みによって、コンパイル済フレームワークをコミットすることで得られるメリットの1つ「間違ったバイナリを使ってしまうことがない」が得られたのではないでしょうか。更にこのやり方であれば、リリースビルドとデバッグビルドを間違えることもなくなります。

今後はこういった開発環境を健全に保つための仕組みづくりをどんどんやっていこうと思っています。こういう取り組みに興味を持っていただけた方、是非ご連絡下さい。お待ちしています。

カテゴリー