こんにちは、モバイル基盤部のヴァンサン(@vincentisambart)です。
Swift Package ManagerはAppleがXcodeで公式にサポートしている唯一のパッケージマネージャーです。Xcode公式サポートの他に、Swift Package Manager形式でのみ提供されているswift-algorithms、swift-atomics、将来的に期待されているswift-async-algorithmsといった準標準ライブラリを利用できるようになるという大きなメリットがあります。
クックパッドiOSアプリ(以下クックパッドアプリ)で一部の依存パッケージをXcodeのSwift Package Manager対応を使って入れるようにしました。この導入で得たいくつかの知見をまとめました。
XcodeのSwift Package Manager対応
本来のSwift Package ManagerがSwiftプロジェクトの一部です。コマンドラインでswift package
で使えます。活用するプロジェクトの構成をPackage.swift
で定義します。
ですが、今回話したいのはSwift Package ManagerのパッケージをXcodeのプロジェクトで使う時の話です。依存されているパッケージのプロジェクト構成がPackage.swift
で定義されていますが、メインのプロジェクトの構成がXcodeプロジェクト(xcworkspaceまたはxcodeproj)で定義されています。
Swift Package Managerがオープンソースであるのに対し、Xcode側の実装はオープンソースのSwift Package Managerの一部を使いながらもクローズドソースです。
プロジェクト構成の定義がPackage.swift
ではないので、swift package
コマンドが使えません。Xcode内ではプロジェクト設定のPackage Dependenciesタブで依存パッケージを変更できます。
ターゲットの設定では、Build PhasesのLink Binary with Librariesに使いたいパッケージのライブラリを指定します。
また、XcodeのFile > Packagesメニューにキャッシュリセットやパッケージ更新用のコマンドがあります。
パッケージ自体はディレクトリ構造をSwift Package Managerの期待した構造に合わせると、Package.swift
が割りと作成しやすいと思いますが、もっと詳しく説明すると長くなるので今回はしません。1つだけ不自然な点を挙げると、なぜかPackage.swift
で未対応なOSを指定できないようです。platforms
でiOS
だけを指定しても、macOSが未対応になるわけではなく、macOSに関して最低OSバージョンがデフォルトのものになるだけです。
最近までの流れ
以前からクックパッドアプリでSwift Package Managerを導入をしようとしていました。hiragramの記事で説明されているように2020年12月に試みましたが、クックパッドアプリで使える状態にまだ達していなかったため断念しました。
2021年12月リリースのXcode 13.2.1では、複雑な依存関係だと手元でのビルドに問題なかったが、AdHocやApp Store配布用のエキスポートが失敗していました。ですが、先月2022年4月にリリースされたXcode 13.3.1でXcode 13.2.1で起きていた問題が解消されて、クックパッドアプリでついに使えるようになりました。
社内ライブラリで使うには
最初に試したとき、GitHub.comなどに公開されているパッケージはXcodeのデフォルト設定でSwift Package Manager対応を利用してビルドできましたが、社内ライブラリではうまく動きませんでした。これだと導入のメリットがだいぶ限られてしまいます。
Xcodeが非公開レポジトリを取得するとき、デフォルト設定では、~/.ssh
に入ったシステム標準の設定が使われるのではなく、Xcode独自の仕組みが使われます。クックパッドでは、リモートで働くとき、VPNまたはSSHトンネル使う必要があります。開発者のマシンでシステムのSSHが既に設定されていますし、複雑なSSH設定はXcode独自の仕組みでできないので、XcodeがシステムのSSHの設定を使ってほしかったです。幸いなことに、このような設定があります。すべての開発者の手元で以下のコマンドを実行すればXcodeの設定を変えられます。
defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES
この件に関するネットでの記事を見ると、上記のコマンドの頭にsudo
が入った記事もありますが、僕の試した限りでは逆にsudo
が入っていると効果がありませんでした。YES
の代わりに1
を使っても問題ありません。
すべての開発者の手元で実行されるようにしなければいけないのは不便なので、プロジェクト作成、環境設定、依存パッケージインストールのようなスクリプトがあれば、そこでやるのがおすすめです。
# 現在の設定を確認する # (`|| true`は`set -e`を使ってもエラーにならないため) using_system_ssh=`defaults read com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM 2> /dev/null || true` # まだ設定されていない場合のみ設定を変える if [ "$using_system_ssh" != "YES" ] && [ "$using_system_ssh" != "1" ]; then defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES fi
Package.resolved
パッケージマネージャーはユーザーが基本的にどういうパッケージのどういうバージョンを使いたいのか指定しますが、バージョンの指定は固定にできるとはいえ、メジャーバージョンが変わらなければ更新しても良いこともよくあります。
パッケージマネージャーは指定を満たすバージョンと明記されていないけど依存されているパッケージを解決します。Swift Package Managerでは、解決されたパッケージのリストとそれぞれのバージョンがPackage.resolved
というJSONファイルに入ります。全ての開発者がそれぞれのパッケージの同じバージョンを使わないとややこしいので、アプリはこのファイルをレポジトリに入れるのを強く推奨します。CocoaPodsでいうとPodfile.lock
、CarthageでいうとCartfile.resolved
、RubyGemsでいうとGemfile.lock
と同じ役目です。
本来のSwift Package Managerでは、Package.resolved
がPackage.swift
と同じディレクトリに入りますが、Xcodeではメインで使われているのがxcworkspaceかxcodeprojかによってディレクトリが少し違います。xcworkspaceの場合、MyProject.xcworkspace/xcshareddata/swiftpm/Package.resolved
ですが、xcodeprojの場合、MyProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
です(もちろん「MyProject」は自分のプロジェクト名に置き換えます)。
xcworkspaceの場合、必ずxcworkspaceを開けるように気を使いましょう。xcodebuild
もいつもxcworkspaceを指定するように。
Xcodeでパッケージがうまく取得できていない時
Xcodeでプロジェクトを開く時、必要であればXcodeがプロジェクトを取得しようとしますが、それが失敗する時はたまにあります。そういう時、XcodeのメニューにあるFile > Packages > Reset Package Cachesがおすすめです。
xcodebuild
Xcodeのユーザー体験はXcode自体のUIがメインですが、CIやスクリプトは基本的にxcodebuild
を使います。xcodebuild
とXcodeのSwift Package Manager対応絡みで気を使う必要のある点がいくつかあります。
SSH認証
Xcode自体と違って最近xcodebuild
がデフォルトでシステムのSSH設定を使うようになっています。念のために明記したい場合、-scmProvider system
でできます(SCM=Source Code Management)。(逆にXcode独自の認証方法は-scmProvider xcode
で指定できます)
依存パッケージ解決
Package.resolved
がない場合、依存パッケージ解決がまず依存パッケージやバージョン指定を見てPackage.resolved
を生成してくれます。Package.resolved
の情報があれば、それに従って依存パッケージ解決が必要なパッケージバージョンを取得してくれます。
Xcodeでプロジェクトを開く時やxcodebuildでビルドをする時に必要であれば依存パッケージ解決が自動的に行われるが、xcodebuildで依存パッケージ解決だけをしたい場合以下のコマンドでできます。
xcodebuild -resolvePackageDependencies -workspace MyProject.xcworkspace -scheme MyProject
明確でありたければ-scmProvider system
を指定できますし、取得されたパッケージの保存先を-clonedSourcePackagesDirPath
で指定できます。
キャッシュ
クラウドで動くCIはスピードを出すにはキャッシュをうまく活用するのが大事です。xcodebuild
で依存パッケージの取得を簡単にキャッシュできます。上記に説明されたようにxcodebuild
で依存パッケージ解決を実行して、-clonedSourcePackagesDirPath
で指定されたディレクトリをキャッシュすれば良いです。その後xcodebuild
でビルドコマンドなどを実行する際、改めて-clonedSourcePackagesDirPath
で同じディレクトリを指定するのをお忘れずに。
Package.resolved
の更新
XcodeのメニューにFile > Packages > Update to Latest Package Versionsでパッケージを更新できますが、現時点でxcodebuild
にこういう機能がありません。
クックパッドアプリは毎週自動的に全てのパッケージを更新してPRを出すCIジョブがあります。このジョブのためxcodebuild
でパッケージを更新する方法が必要でした。
更新のコマンドがないとはいえ、少し強引ではありますが、方法がないわけではありません。Package.resolved
がなければ、依存パッケージ解決が最新パッケージを取りに行きます。試してみると、キャッシュがあれば最新のバージョンではなく以前と同じバージョンになってしまうのでxcodebuild
がキャッシュを見に行かないようにする必要があります。
# Package.resolvedを消す rm -f MyProject.xcworkspace/xcshareddata/swiftpm/Package.resolved # 空っぽなテンポラリディレクトリを作成する tmpcache=`mktemp -d` # 依存パッケージ解決をやる。残っていたキャッシュが使われないために、キャッシュディレクトリに作ったばかりのテンポラリディレクトリを指定する xcodebuild -resolvePackageDependencies -workspace MyProject.xcworkspace -scheme MyProject -scmProvider system -clonedSourcePackagesDirPath "$tmpcache" # テンポラリディレクトリを消しておく rm -rf "$tmpcache" # Package.resolvedに指定を満たすパッケージの最新のバージョンが使われるようになったはず
XcodeGenを使う場合気をつけること
以前giginetが説明したようにクックパッドアプリのプロジェクト構成はXcodeGenで定義しています。
プロジェクト構成を直接xcworkspaceまたはxcodeprojで定義する場合、Xcode内で依存関係に変更を加えるとPackage.resolved
が自動的に更新されますが、XcodeGenのプロジェクト設定を変えるだけでPackage.resolved
が更新されません。依存関係に変更を加えて、XcodeGenを実行してから、Package.resolved
を更新するにはXcodeを開くかxcodebuildで依存パッケージ解決をするか、が必要がです。気をつけないとPackage.resolved
の変更が入っていないPRを出すリスクが出てしまいます。
また、最近クックパッドアプリでライセンス管理にLicensePlistを使用していますが、LicensePlistが見ているのもPackage.resolved
です。Package.resolved
が更新されていないとLicensePlistの生成したファイルもプロジェクトファイルに入った依存関係設定と一致しません。
プロジェクト生成のスクリプトではXcodeGenを実行してからLicensePlistを実行していましたが、上記の理由で、LicensePlistを実行する前にxcodebuild
の依存パッケージ解決をするようにしました。パッケージ解決が既に完了している場合プロジェクト生成スクリプトの実行時間が2~3秒伸びるのは少し残念ですが、分かりにくい状態を避ける方が大事だと思います。
他のパッケージマネージャーとの絡み
クックパッドアプリはCocoaPodsとCarthage両方を使っていましたが、ライブラリが複数なパッケージマネージャーをサポートしている場合、ライブラリを導入するときはどれを使うべきか悩んでしまいます。特定なパッケージマネージャーに寄せた方が運用しやすいです。
最近Appleプラットフォーム開発界隈が依存関係をSwift Package Managerに寄せつつあるように感じるので、クックパッドアプリもできるだけSwift Package Managerに寄せることにしました。
別のパッケージマネージャーからSwift Package Managerに移行するとき、ライブラリがPackage.swift
を既に提供していると、アプリのプロジェクトに参照のやり方を変えるだけのはずですが、他の変更も必要になることがあります。
例えば、Carthageを使って作成されたxcframeworkからSwift Package Managerに移行したら、クックパッドアプリでimport
を足す必要があったSwiftファイルがありました。モジュールの扱い方の違いで、Carthage版だとライブラリをimport
するだけでFoundationやUIKitが暗黙的にimport
されることがありました。Swift Package Manager版だとそんなことがないので、そのimport
を明記する必要があります。
また、クックパッドアプリのようにアプリが複数のモジュールで構成されている場合、パッケージマネージャーを変えることで、どのモジュールがどのパッケージを参照するのか少し変える必要があることがあります。基本的にSwift Package Managerの場合、参照をもっと多くの箇所で明記する必要がありました。
パッケージマネージャーとの絡みでのハマりどころといえば、XcodeのSwift Package Manager対応とCocoaPodsを併用している場合、XcodeでターゲットのBuild PhasesのDependenciesにSwiftパッケージが明記されていると、CocoaPods実行時に例外が発生してしまいます。CocoaPodsが使っているXcodeprojライブラリのバグです。修正はしましたが、現時点でこの修正が入ったXcodeprojのバージョンがまだリリースされていません(現時点で最新のリリースが昨年8月にリリースされた1.21.0)。ワークアラウンドとして、SwiftパッケージがターゲットのBuild PhasesのLink Binary with Librariesに入っていれば、Dependenciesに入っていなくても暗黙的に依存されるのでLink Binary with Librariesから消せば良いはずです。一応Xcodeprojの特定なコミットハッシュに依存するのも選択肢の1つです。
最後に
課題点がいくつかありましたが、それを越えればXcodeのSwift Package Manager対応が割りと便利だと思います。パッケージ作成はCocoaPodsと違ってスペックをどこかにアップロードする必要ありませんし、Package.swift
でパッケージ構成定義も楽です。
クックパッドアプリでは少しずつSwift Package Managerの利用を増やそうとしています。今週のリリースでCarthageを使わなくなってSwift Package Manager + CocoaPodsの構成になりました。