cockscomblog?

cockscomb on hatena blog

XCFrameworkをSwift PackageとしてGitHubでリリースする

以前、XCFrameworkをバイナリターゲットとしてSwift Packageに埋め込んだライブラリを作った。

これを容易に利用するためには、ビルド済みのXCFrameworkをどこかにアップロードして、それを参照するSwift Packageを公開するとよい。XCFrameworkはGitHubリリースに成果物として置いておくことにする。

ということで、前回は概ねこれを手作業でやったのだけど、手作業したくないのでGitHub Actionsにした。

リリースが自動化されているソフトウェアは信頼性が高い。

GitHub Actionsでバイナリターゲットを含むSwift Packageをリリース

自動化にあたって手順を書き出してみると、次のようになる。ややこしい。

  1. XCFrameworkをビルドしてアーカイブする
  2. XCFrameworkのチェックサムを計算
  3. バージョン番号を決める
  4. Package.swiftを書き換える
  5. Package.swiftをコミット
  6. タグを打つ
  7. GitHubでリリースを作成

実際のActionはこうなる。

順を追ってやっていく。

XCFrameworkをビルドしてアーカイブする

これは普通にやったらいい。今回はgomobileを使っているのでちょっと変な感じではある。Makefileに書いてある。

.PHONY: install-tools
install-tools:
    go install golang.org/x/mobile/cmd/gomobile@latest
    gomobile init

.PHONY: xcframework zip
xcframework: install-tools
    gomobile bind -target=ios,iossimulator,macos,maccatalyst -iosversion 14 -prefix GOJQ -o Frameworks/GOJQBinding.xcframework github.com/cockscomb/swift-gojq/binding

zip: xcframework
    zip -X -r Frameworks/GOJQBinding.xcframework.zip Frameworks/GOJQBinding.xcframework/

XCFrameworkのチェックサムを計算

Swift Packageのバイナリターゲットにはチェックサムが必要なので計算する。swift package compute-checksum するだけ。

- name: Compute checksum
  id: checksum
  run: |
    echo "checksum=$(swift package compute-checksum Frameworks/GOJQBinding.xcframework.zip)" >> $GITHUB_OUTPUT

バージョン番号を決める

mathieudutour/github-tag-action を使った。major とか minor とか patch とかで一つ前のバージョン番号から決めることにして、workflow_dispatchinput で選べる感じにしている。

on:
  workflow_dispatch:
    inputs:
      bump:
        description: 'Bump version'
        required: true
        default: 'patch'
        type: choice
        options:
        - major
        - minor
        - patch
- name: Calculate next version
  id: next_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    default_bump: ${{ github.event.inputs.bump }}
    tag_prefix: ''
    dry_run: true

バージョンを先に決めておかないとリリースのURLが決まらないので、先にバージョンを決める。dry_run があって便利。

Package.swiftを書き換える

ここがいちばんトリッキー。リリースのURLとチェックサムPackage.swift に書き込まないといけない。Package.swift のテンプレートを用意してレンダリングするか、すでにある Package.swift を書き換えるかだが、テンプレートを管理するのが面倒なので後者を選ぶ。

丁寧にSwift Package Pluginを作ってある。Swift Syntaxで Package.swift を書き換える。

Swift SyntaxによるPackage.swiftの書き換え

Swift Syntaxでは、SyntaxRewriter を継承して適当な visit(_:) メソッドを実装すると、ASTを辿って必要な箇所を書き換えられる。Sources/swift-package-checksum-rewriter/BinaryTargetSourceRewriter.swift として実装している。書き換え前後で余計な差分が出にくいように Trivia というやつで改行や空白を丁寧に調整している。(それもあってSwiftSyntaxBuilderがあまり思うように使えていない。)

これを executableTarget にしておいて、Swift Package Pluginからは実行ファイルとして呼び出す。だからプラグインとしての実装は次の通りで、ただのラッパーになった。

import Foundation
import PackagePlugin

@main
struct RewriteChecksumCommandPlugin: CommandPlugin {
    func performCommand(context: PluginContext, arguments: [String]) async throws {
        let tool = try context.tool(named: "swift-package-checksum-rewriter")
        let process = try Process.run(URL(fileURLWithPath: tool.path.string), arguments: arguments)
        process.waitUntilExit()
    }
}

使い方

これを使うには Package.swift に依存を追加する。

     dependencies: [
        .package(url: "https://github.com/cockscomb/swift-package-checksum-rewriter", from: "0.1.0"),
    ],

あとは swift package コマンドから呼び出すだけ。--allow-writing-to-package-directory するのがコツ。

- name: Rewrite Package.swift
  run: |
    swift package \
      --allow-writing-to-package-directory \
      rewrite-package-binary-target \
      --url=https://github.com/cockscomb/swift-gojq/releases/download/${{ steps.next_version.outputs.new_tag }}/GOJQBinding.xcframework.zip \
      --checksum=${{ steps.checksum.outputs.checksum }} \
      Package.swift \
      GOJQBinding

ここが一番面倒だった。

Package.swiftをコミット

ここはどうやってもいい。今回は stefanzweifel/git-auto-commit-action を使った。簡単。

- uses: stefanzweifel/git-auto-commit-action@v4
  with:
    commit_message: Bump version to ${{ steps.next_version.outputs.new_tag }}

タグを打つ

さっきは dry_run したけど今回が本番。

- name: Bump version
  id: bump_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    default_bump: ${{ github.event.inputs.bump }}
    tag_prefix: ''

GitHubでリリースを作成

actions/upload-release-assetアーカイブされているので、案内に従って softprops/action-gh-release を使う。これも簡単。

- name: Release
  uses: softprops/action-gh-release@v1
  with:
    tag_name: ${{ steps.next_version.outputs.new_tag }}
    body: ${{ steps.next_version.outputs.changelog }}
    files: Frameworks/GOJQBinding.xcframework.zip

完成

結構ややこしい感じだけど、これでバイナリターゲットを含むSwift Packageを自動的にリリースできるようになった。

ちょっとやったらできるだろうと思って作業し始めたけど、Package.swiftの書き換えがややこしくて思ったより大変だった。