見出し画像

またあう日までさようなら。 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 で管理

というふうにかわりました。

やったこと

やったこと自体はシンプルで着実に一歩ずつすすめていったという内容になります。

  1. SPM で入れられるライブラリは SPM で入れる

  2. Xcode Cloud とかでなんかうごかなくなるぞ?の対応

  3. CocoaPods にしかないライブラリの対応

  4. 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 のようなツールの対応

一番の問題はこいつらです。今、とりうる対応としては、以下のようなものがあると考えられます。

  1. HomeBrew でいれちゃおう

  2. CocoaPods がアプリ自体に絡んできているわけではないのでスルーする

  3. 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 とかを全部削除しておしまいです。

まとめ

こんなかんじで、

  1. SPM で入れられるライブラリは SPM で入れる

  2. Xcode Cloud とかでなんかうごかなくなるぞ?の対応

  3. CocoaPods にしかないライブラリの対応

  4. SwiftGen や SwiftLint のようなツールの対応

こまごま問題はありましたし、Permission 問題とか resolve 問題があって完璧とはいえない状態ですが、CocoaPods と別れを告げて Swift Package Manager と XcodeGen を使っていけるようになりました。

次の目標

さようなら。XcodeGen っていう記事を書きたいですが、まだまだ、時間はかかりそうです。ただ、そのまえに、Slash コマンドも書きたいですねぇ。