Swift Package ManagerでRenovateを利用する際の工夫点

はじめに

こんにちは。クラスター株式会社でソフトウェアエンジニアをしているTAATです。

今回はクラスターで導入しているRenovateというパッケージの自動更新ツールをSwift Package Managerで利用する上での問題点やその解決策について紹介します。

Renovateとは?

Renovateとは、パッケージの自動更新を行うツールで、利用するパッケージに新しいバージョンがリリースされると、自動でアップデートのPull Requestを作成してくれて、リリースノートや変更内容も記載してくれます。しかし、Swift Package Managerには対応しているものの、以下の問題点がありました。

  1. Package.swift内のパッケージは更新するが、Xcodeプロジェクトから入れたパッケージは更新してくれない
  2. 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を定義すると良さそうです。

team-blog.mitene.us

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で更新してコミット・プッシュする方法があります。こちらの記事で紹介されている内容を参考にワークフローを作成します。

zenn.dev

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で失敗してしまいました。

tech-blog.cluster.mu

そこで、ジョブ実行時に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が公式で対応してくれることが一番ですが、同じようなことで困っている場合はぜひ参考にしてみてください。

クラスター社に興味を持ったら、ぜひ会社・採用ページをご覧ください!

corp.cluster.mu