はじめに
こんにちは。クラスター株式会社でソフトウェアエンジニアをしているTAATです。
今回はクラスターで導入しているRenovateというパッケージの自動更新ツールをSwift Package Managerで利用する上での問題点やその解決策について紹介します。
Renovateとは?
Renovateとは、パッケージの自動更新を行うツールで、利用するパッケージに新しいバージョンがリリースされると、自動でアップデートのPull Requestを作成してくれて、リリースノートや変更内容も記載してくれます。しかし、Swift Package Managerには対応しているものの、以下の問題点がありました。
- Package.swift内のパッケージは更新するが、Xcodeプロジェクトから入れたパッケージは更新してくれない
- Package.swiftは更新するが、Package.resolvedは更新してくれない
- issueはあるものの、まだ対応中のようです
- 自分で追いコミットをする必要があり、設定によっては別のレビュアーに承認してもらう必要がある
依存関係をPackageに切り出す
問題点1を解決するために、Xcodeプロジェクトからパッケージを入れるのではなく、依存するパッケージを管理するためのパッケージを作成してプロジェクトに取り込みます。
まずはXcodeメニューのFile > New > Packageから新しいパッケージを追加して(今回はClusterPackage)、配置したいディレクトリでAdd Files to “ターゲット名”から追加したパッケージを選択して取り込みます。
新しいパッケージをアプリターゲットのFrameworks, Libraries, and Embedded Contentに追加することで、新しいパッケージで指定したパッケージを利用することができます。テストでも利用する場合は、テストターゲットにも追加する必要があります。
次に新しいパッケージのPackage.swiftを編集していきますが、targetsのdependenciesを指定する際のタイポを防ぐために、以下のようにextensionを定義すると良さそうです。
import PackageDescription // targetsのdependenciesで指定する時に補完が効く private extension PackageDescription.Target.Dependency { static let swiftProtobuf: Self = .product(name: "SwiftProtobuf", package: "swift-protobuf") static let sdWebImage: Self = .product(name: "SDWebImage", package: "SDWebImage") static let sdWebImageSwiftUI: Self = .product(name: "SDWebImageSwiftUI", package: "SDWebImageSwiftUI") static let alamofire: Self = .product(name: "Alamofire", package: "Alamofire") static let lottie: Self = .product(name: "Lottie", package: "lottie-spm") static let shimmer: Self = .product(name: "Shimmer", package: "SwiftUI-Shimmer") // ... } // R.swiftを利用するためのプライグインを定義 private extension PackageDescription.Target.PluginUsage { static let rswiftGenerateInternalResource: Self = .plugin( name: "RswiftGenerateInternalResources", package: "R.swift") } let package = Package( name: "ClusterPackage", platforms: [.iOS(.v16)], products: [ .library( name: "ClusterPackage", targets: ["ClusterPackage"]), .library( name: "ClusterTestPackage", targets: ["ClusterTestPackage"]) ], dependencies: [ .package(url: "<https://github.com/apple/swift-protobuf.git>", from: "1.11.0"), .package(url: "<https://github.com/SDWebImage/SDWebImage.git>", from: "5.18.7"), .package(url: "<https://github.com/SDWebImage/SDWebImageSwiftUI.git>", from: "2.2.6"), .package(url: "<https://github.com/Alamofire/Alamofire.git>", from: "5.8.1"), .package(url: "<https://github.com/airbnb/lottie-spm.git>", from: "4.3.4"), .package(url: "<https://github.com/markiv/SwiftUI-Shimmer>", from: "1.4.0"), // ... ], targets: [ .target( name: "ClusterPackage", dependencies: [ .swiftProtobuf, .sdWebImage, .sdWebImageSwiftUI, .alamofire, .lottie, .shimmer, // ... ], resources: [.process("Resources")], plugins: [.rswiftGenerateInternalResource]), .target( name: "ClusterTestPackage", dependencies: [ // ... ]) ])
注意点として、新しいパッケージの各ターゲット(今回はSources/ClusterPackage, Sources/ClusterTestPackage)には1つ以上のSwiftファイルが必要ですが、空のファイルでも大丈夫です。また、ResourcesディレクトリでもSwiftファイルがないと、R.generated.swiftでエラーが出るので、こちらも空のファイルを追加しておきます。
これで依存するパッケージを管理するパッケージを追加できたので、Renovateが更新してくれるようになりましたが、まだPackage.resolvedは更新してくれない問題が残っています。
GitHub ActionsでPackage.resolvedを更新する
問題点2を解決するために、自動更新されないPackage.resolvedをGitHub Actionsで更新してコミット・プッシュする方法があります。こちらの記事で紹介されている内容を参考にワークフローを作成します。
name: Update Package.resolved on: pull_request: types: [opened, synchronize, reopened] paths: - Package.swiftのファイルパス env: DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer defaults: run: working-directory: プロジェクトのディレクトリ jobs: preupdate: runs-on: ubuntu-latest timeout-minutes: 10 outputs: files_changed: ${{ steps.file_changes.outputs.files }} if: contains(github.head_ref, 'renovate') steps: - uses: actions/checkout@v4 - name: File changes id: file_changes uses: trilom/file-changes-action@v1.2.4 with: output: ',' update: runs-on: macos-13 timeout-minutes: 30 needs: preupdate # preupdateでのファイル差分にPackage.resolvedが含まれていればスキップ if: contains(needs.preupdate.outputs.files_changed, 'Package.resolved') == false steps: - uses: actions/checkout@v4 with: # git push時にdetached HEADになるため、refを指定してマージ元のブランチをチェックアウト ref: ${{ github.head_ref }} - name: Resolve package dependencies run: | # 依存関係を解決 xcodebuild -resolvePackageDependencies -workspace ワークスペース名 -scheme スキーム名 - name: Commit and push env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add Package.resolvedのファイルパス git commit -m "[Renovate] Updated Package.resolved." git clean -df git push
トリガーとして対象パッケージのPackage.swiftを含むPull Requestを指定し、preupdate, updateジョブを実行します。なお、Xcodeのパスは環境変数DEVELOPER_DIRで指定できます(こちらを参照)。
- preupdate
- Pull Requestのファイル差分を取得する
- update
- Pull Requestのファイル差分にPackage.resolvedがある場合はスキップ
- xcodebuild -resolvePackageDependenciesで依存関係を解決してPackage.resolvedを更新する
- github-actions[bot]ユーザーとしてコミット・プッシュする
しかし、ここで1つ問題があります。ClusterPackageをXcodeプロジェクトに取り込んでいるので、xcodebuild -resolvePackageDependenciesで依存関係を解決する必要があります(パッケージ単体であればswift package resolveで依存関係を解決できます)が、clusterの場合こちらの発表でも紹介したようにUnity as a LibraryとしてUnityFramework.xcframeworkもXcodeプロジェクトに取り込んでいますが、ファイルサイズが大きいためgitignoreに追加されており、環境構築時に手元でビルドしているため、ジョブのxcodebuildで失敗してしまいました。
そこで、ジョブ実行時にUnityFramework.xcframeworkの代わりにダミーのxcframeworkを用意して差し替えることにしました。詳細は省略しますが、UnityFrameworkのターゲットを持つダミーのUnity-iPhone.xcodeprojを作成して、以下のようなSetupステップを依存関係解決の前に追加することで、問題なくxcodebuild -resolvePackageDependenciesが実行できるようになります。補足ですが、このダミーのUnity-iPhone.xcodeprojは元々シミュレータービルド用に用意されたもので、今回はそれを活用しています。
- name: Setup run: | xcodebuild archive -project $xcodeproj -scheme $scheme \ -destination 'generic/platform=iOS Simulator' \ -archivePath $archivePath \ SKIP_INSTALL=NO \ BUILD_LIBRARY_FOR_DISTRIBUTION=YES rm -rf $xcframework xcodebuild -create-xcframework \ -framework $archivedFrameworkPath \ -output $xcframework
これでようやくパッケージに切り出した依存するパッケージに対しても、Renovateが更新してくれるようになり、さらにGitHub Actionsで自動でPackage.resolvedも更新してくれるようになりました。
おわりに
Swift Package ManagerでRenovateを利用する上での問題点と解決策として、Xcodeプロジェクトで入れているパッケージは更新してくれないので別パッケージに切り出すと良い、さらにPackage.resolvedはGitHub Actionsで更新できることについて紹介しました。Renovateが公式で対応してくれることが一番ですが、同じようなことで困っている場合はぜひ参考にしてみてください。
クラスター社に興味を持ったら、ぜひ会社・採用ページをご覧ください!