またあう日までさようなら。 CocoaPods
おはようございます。9月の後半くらいから教習所に通い始めたnoteのwaturaです。この記事をかいているときは、仮免に合格してはじめての路上を目前に控えている状態です。
MagicPod の記事では、次は Xcode Cloud を Slack Slash コマンドから呼び出すよ!というのを書くと書いていたんですが、秋なので note の iOS アプリから CocoaPods 依存をなくしてみました。
最終的な目標
CocoaPods を取り除くことは、最終的な目標ではなくその通過点です。なので、ちょっと、CocoaPods 使うのと変わらないんじゃないの? もしかしたら、CocoaPods 使っていたほうがシンプルだったんじゃない?というところがありますが、Swift Package Manager をフルにつかった Multi Module 構成にするという目標のために一時的になっているだけです。
ただ、残念なことにまだまだ道のりは遠いのですが…
CocoaPods と別れを告げるまで
XcodeGen を使って xcodeproject 生成や、 SPM のパッケージ管理
CocoaPods をつかったライブラリ管理
CocoaPods がつくった xcworkspace で開発
CocoaPods で入れた SwiftGen/SwiftLint/SwiftFormat を開発時に利用
一応、いつか CocoaPods と別れを告げるぞ!という気持ちでちょっとずつ SPMでライブラリを管理するようにしていました。そのため、一部 XcodeGen 一部 CocoaPods という状態でした。
CocoaPods がいなくなった結果
XcodeGen を使って xcodeproj 生成や、 SPM のパッケージ管理
一部の SPM パッケージは Package.swift で管理
というふうにかわりました。
やったこと
やったこと自体はシンプルで着実に一歩ずつすすめていったという内容になります。
SPM で入れられるライブラリは SPM で入れる
Xcode Cloud とかでなんかうごかなくなるぞ?の対応
CocoaPods にしかないライブラリの対応
SwiftGen や SwiftLint のようなツールの対応
1. SPM で入れられるライブラリは SPM で入れる
まず、最初は SPM でも提供されているライブラリを、CocoaPods 管理から SPM 管理に変えていくことでした。過去にも CocoaPods から SPM に移すぞ!という機運はあったと思うのですが、まだ、対応していなかったりなど、なんらかの理由でスルーされていたライブラリがいくつか残っていました。
バージョンアップすらされていないライブラリがあったりもしたので、その対応を行いつつ、SPM 版に切り替えていきました。
残ったライブラリ等としては
Canva.DesignButton
SwiftLint
SwiftGen
SwiftFormat
残った理由としては、SPM 対応していない、実行プログラムなので、SPMに対応していても、単純には管理できないからです。
2. Xcode Cloud とかでなんかうごかなくなるぞ?の対応
SPM で管理できるものは、SPM で管理するという状態にまでもっていけました。
このころ、Xcode Cloud がパブリックになってきて、利用できるようになったので試していたのですが、依存関係まわりでエラーがでるようになってしまいました。
上記の記事ではCode Sign だの Embed だのをいじって、なんとかしようとしていたのですが、もっとシンプルにすっきりできました。
依存関係の解消
XcodeGen をつかって、ある程度の Multi Module 構成を構築しています。そのため、ライブラリが複数のモジュールでつかわれる場合があります。それが上記記事でかいた、細々したものです。
今回は、XcodeGen の見直しを含めて、モジュールで使われるライブラリを一番底に当たるモジュールから @_exported import してさわれるようにするという方法で解消しました。
ただ、 @_exported 自体が公式の方法ではなさそうなので、公式の方法を探すというタスクをこそこそとやり続けています。
なんと大昔に、ドキュメントから削除されてるんですよねー
Static と Dynamic をうまく決めてくれない
Package.swift でDynamic(Static) が指定されたライブラリの場合、なぜかXcode Cloud のSPMではうまく取り回すことができませんでした。手元で動かす分や fastlane でビルドする分には問題なく動いてはいたので、なかなか気がつけませんでした。これの解決策としては、Package.swift では、
type を指定せずに、Xcode/SPM に選ばせるようにする
type が必要な場合は別途 type を指定した products をつくる
とする必要がありました。なので、導入したライブラリ側に type 指定なしの products を追加してもらうという方法で対処しました。
products: [
.library(name: "SomeLibrary", type: .dynamic, targets: ["SomeLibrary"])
]
↑だとLinkとかするときにうまくできないときがあるので、↓みたいに、type なしする必要があります。また、Dynamic(Static) を選ばないといけない場合があるみたいなときは、SomeLibraryDynamic(Static) を作っておくと、選べるようになっていい感じです。
products: [
.library(name: "SomeLibrary", targets: ["SomeLibrary"]),
.library(name: "SomeLibraryDynamic", type: .dynamic, targets: ["SomeLibrary"]),
.library(name: "SomeLibraryStatic", type: .static, targets: ["SomeLibrary"])
]
3. CocoaPods にしか対応していないライブラリ
note の場合は Canva. DesignButton が CocoaPods にしか対応していないライブラリでした。とりあえず、まずは、Canva に SPM 対応の要望をだしつつ、自分たちでもできる対応をするという方針にしました。
CocoaPods にしか対応していないライブラリでも、SPM から扱えるようにできる方法があります。
この記事はかなりモリモリな記事で、これさえあればどんなライブラリもコワクナイレベルなんですが、Canva Button の場合は、XCFramework 形式で配布されていたので、こんなカオスで大変そうなことをするまでもなく楽でした。
まず、SPMにできないライブラリ用のリポジトリ等を作って、その中で作業します。ライブラリをインストールするように記載した、Podfile をつくります。
そのとき、上記の記事にもありますが、Podfile に integrate_targets: false を指定すると xcworkspace が作られなくなるので、それを利用します。
そして、こういう Package.swift をつくります。今回は簡単のために、リポジトリに Pods ディレクトリを含めてしまって、それを使うようにしています。
最後に、このリポジトリをメインアプリの SPM で管理するようにしておけば、CocoaPods でしか配信されていなかった、Canva.DesignButton も SPM から管理できるようになります。
あとは、Canva.DesignButton 用を更新しつづけるためのスクリプトとかなんやかんやがあると最高なんですが、今回はやっていません。
これで、CocoaPods で管理しているのは、SwiftLint, SwiftGen, SwiftFormat だけとなりました。こっちの Podfile にも integrate_targets: false を追加したので、xcworkspace ではなく xcodeproj だけをつかうようにできました。
4. SwiftGen や SwiftLint のようなツールの対応
一番の問題はこいつらです。今、とりうる対応としては、以下のようなものがあると考えられます。
HomeBrew でいれちゃおう
CocoaPods がアプリ自体に絡んできているわけではないのでスルーする
SPM でなんとか管理する
せっかく、バージョン管理ができている状態なのに、HomeBrew にうつすメリットがないので、1 はありえませんでした。なので実質 2、3 どっちにするのがいいかというところです。最終的に 3 を選んだので、CocoaPods と別れを告げられました。
しかし、Swift 5.7 の現在、Package.swift から任意のツールを実行するような post build script をいいかんじに設定する方法がありません。
これでは、SPM オンリーにする目標を達成したときに、SwiftXXX を今のようなタイミングで呼び出せないということになります。
なので、3 の SPM でなんとか管理するという方法をとることにしました。
なお、呼び出すタイミングは手動とか git hook とかで十分だよ!というのならば、CocoaPods を使い続けたほうが、シンプルだしきれいな感じになると思います。
SPM には、ツール等を実行するための 3 種類のプラグインシステムがあります。
Build Tool Plugin
Command Line Plugin
Xcode Project Plugin
SwiftGenPlugin をつかって SwiftGen を導入するようにしました。このリポジトリは、上記の 3 種類のプラグインすべてが実装されています。なので、自前でプラグインを作る上でもすごく参考になりました。
このリポジトリでは実装されている Xcode Project Plugin は使う予定がなかったので、軽い調査くらいしかしていません。
Build Tool Plugin
Package.swift で以下のように指定すると、Pre build や Post build に呼び出せるようになります。(どっちで実行されるかはプラグインの実装依存)将来的には、3つの SwiftXXX を Build Tool Plugin として呼び出せるようにしたいと考えています。が、今は、Build Tool Plugin を呼び出すアプリ側の準備がぜんぜんできていないので、使っていません。
targets: [
.target(
name: "Target",
dependencies: [],
plugins: [
.plugin(name: "SomePlugin", package: "SomePlugin")
]
)
]
Command Line Plugin
こちらは、Terminal から Plugin を実行できるようになります。今回はこれをつかって XcodeGen から Post Build Script を設定して、Plugin を実行しています。↓の Gist のようなコードでつくれるようになります。これは、SwiftLint を CLI で呼び出すための Plugin です。<VERSION> と <CHECKSUM> が固定値になってしまうので、そこの更新は手動かなにかでがんばる必要があります。
同様のプラグインを実装して、SwiftLint, SwiftFormat, SwiftGen を呼び出すようにしました。が、問題がありました。
Permission Error
Plugin はサンドボックス環境で実行されます。そのため、どのフォルダでも自由に読み書きできるわけではなく、--allow-writing-to-package-directory というオプションをセットすると、指定されたごく一部のディレクトリとパッケージディレクトリに読み書きできるようになります。
ただ、それだけでは十分ではありません。たとえば、$SRCROOT/BuildTools/Package.swift みたいなかんじの Package の位置にSwiftGenPlugin を設定した場合、
swift package --package-path BuildTools --allow-writing-to-package-directory generate-code-for-resources --config swiftgen.yml
Error: You don’t have permission to save the file “Strings.swift” in the folder “Assets”.
といったかんじで、Assets に書き込み権限がなく悲しいことになります。これがなかったら、もっと幸せな感じでプラグインをつかっていけたのですがダメでした。
この書き込み権限の対策として、セキュリティを犠牲にする --disable-sandbox というオプションがあるので、それを使うようにしました。XcodeGen の preBuildScripts に以下のように設定し SwiftGen を実行するようにしました。
SDKROOT=(xcrun --sdk macosx --show-sdk-path)
swift package --package-path BuildTools --allow-writing-to-package-directory --disable-sandbox generate-code-for-resources
そのままだと、iOS 向けにビルドされてしまうため、SDKROOT に macosx を指定するようにしています。
なお、上記の SwiftLintCLIPlugin.swift で --no-cache と指定しているのも同様のPermission周りが理由です。
これで、万事解決で幸せになりました。
といいたいところだったのですが、このプラグインを含む Package.swift を Xcode から管理できるようにすると CPU が 100% に張り付いてしまうので、注意が必要です。
CIだけで発生するエラー?
preBuildScripts として上記のコマンドなどを設定していると、並列して Plugin が呼び出される場合があります。Plugin の依存関係の解決やartifactのダウンロードも並列で呼び出されてしまい、エラーが発生する場合があります。しかも、依存関係の解決やビルドが終了した場合発生しないので、あれ??って思いながら、なんどか実行していたらエラーが発生しなくなり、CI でだけ発生するエラーみたいな顔をしだします…
これの対応としては、事前に
swift package --package-path BuildTools resolve
を呼び出して依存関係とかを解決しておくと発生しなくなります。
これで CocoaPods に残っていたツールも全部対応できました。
CocoaPods が必要になる箇所がゼロになったので、Podfile とかを全部削除しておしまいです。
まとめ
こんなかんじで、
SPM で入れられるライブラリは SPM で入れる
Xcode Cloud とかでなんかうごかなくなるぞ?の対応
CocoaPods にしかないライブラリの対応
SwiftGen や SwiftLint のようなツールの対応
こまごま問題はありましたし、Permission 問題とか resolve 問題があって完璧とはいえない状態ですが、CocoaPods と別れを告げて Swift Package Manager と XcodeGen を使っていけるようになりました。
次の目標
さようなら。XcodeGen っていう記事を書きたいですが、まだまだ、時間はかかりそうです。ただ、そのまえに、Slash コマンドも書きたいですねぇ。