GitHub ActionsでC++ライブラリをXCFrameworkにビルドしSwift Packageとして配布する

# はじめに
この記事ではCMakeで管理されているC++のライブラリをXCFrameworkにビルドし、Swift Pacakgeとして配布する一連の流れをGitHub Actionsで自動化するサンプルを紹介します。流れはざっくり以下です。

1. C++ライブラリのリポジトリにタグを打つ
1. 上記をトリガーにSwift Pacakge側のリポジトリのワークフローをトリガーする
    1. C++ライブラリをXCFrameworkにビルドする
    1. そのXCFrameworkをGitHub Release Assetsにアップロードする
    1. 上記のXCFrameworkを参照するようにPackage.swiftを更新しPRを作成する

# 最初からC++ライブラリにPackage.swiftをおけばいいのでは？
今回のようなシンプルな例だとそれでもいいのですが、実際のプロジェクトに後からPackage.swiftを置こうとすると、C++側の依存の解決やビルドが通らなかったり、色々めんどくさいことあったのでこのような構成になっています。

# 今回利用するサンプルコード
## C++ライブラリ

https://github.com/NakaokaRei/CxxLibSample

構造は以下のようになっています。足し算をする`add`関数が存在するだけの簡単なプロジェクトです。タグが打たれるとSwift Package側にイベントをdispatchします。

```
.
├── .github
│   └── workflows
│       └── dispatch.yml
├── CMakeLists.txt
├── build_xcframework.sh
├── include
│   ├── module.modulemap
│   └── sample.h
└── source
    └── sample.cpp
```


## Swift Package
https://github.com/NakaokaRei/SwiftPackageXCFrameworkSample

ビルドしたXCFrameworkはGitHub Release AssetsにアップロードされbinaryTargetで取得しています。また、Privateリポジトリにも対応できるようにURLは`github.com`ではなく`api.github.com`としています。[^1]

[^1]:[プライベートリポジトリのRelease AssetsにXCFrameworkをホストして、Swift Package Manager・CocoaPodsで配布する](https://qiita.com/thenagain/items/be148744b3c65b370b2e)


```swift
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let zipUrl = "https://api.github.com/repos/NakaokaRei/SwiftPackageXCFrameworkSample/releases/assets/xxxxxxxx.zip"
let checksum = "xxxxxxxxxxxx"

let package = Package(
    name: "SwiftPackageXCFrameworkSample",
    products: [
        .library(
            name: "SwiftPackageXCFrameworkSample",
            targets: ["CxxLibSample"]
        ),
    ],
    targets: [
        .binaryTarget(name: "CxxLibSample", url: zipUrl, checksum: checksum),
        .testTarget(
            name: "SwiftPackageXCFrameworkSampleTests",
            dependencies: ["CxxLibSample"],
            swiftSettings: [.interoperabilityMode(.Cxx)]
        ),
    ],
    cLanguageStandard: .c11,
    cxxLanguageStandard: .cxx20
)
```

# C++ライブラリのリポジトリからSwift Package側にイベントをdispatchする

GitHub ActionsのAPIを使い別のリポジトリにイベントをdispatchします。

https://docs.github.com/ja/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-dispatch-event

以下のようにタグがプッシュされた時に上記のAPIを叩きます。

```(C++ライブラリ側) dispatch.yml
name: Trigger Release Workflow

on:
  push:
    tags:
      - '*.*.*'

jobs:
  trigger_workflow:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Release Workflow
        env:
          PAT: ${{ secrets.PAT_GITHUB }}
          EVENT_TYPE: release-cxxlibsample
          TAG_NAME: ${{ github.ref }}
        run: |
          run: |
          curl -X POST \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer $PAT" \
            https://api.github.com/repos/NakaokaRei/SwiftPackageXCFrameworkSample/dispatches \
            -d '{"event_type": "'"$EVENT_TYPE"'", "client_payload": {"tag": "'"$TAG_NAME"'"}}'
```

# C++のライブラリをXCFrameworkとしてビルドする
CMakeで静的ライブラリのターゲットが定義されているので、`cmake --build`で静的バイナリをビルドしその後`xcodebuild -create-xcframework`でXCFrameworkを生成します。xcframeworkを生成する全体のスクリプトは以下から確認できます。

https://github.com/NakaokaRei/CxxLibSample/blob/master/build_xcframework.sh


```CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(CxxLibSample VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES)

add_library(CxxLibSample STATIC
    source/sample.cpp
    include/sample.h
)

target_include_directories(CxxLibSample PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
```

# XCFrameworkをGitHub Release AssetsにアップロードしPackage.swiftを更新する
以下のGitHub Actionsで`create_release`以降のステップになります。

```(Swift Package側) release.yml
name: Release Swift Package

on:
  repository_dispatch:
    types: [release-cxxlibsample]

jobs:
  build_and_release:
    runs-on: macos-latest

    steps:
      - name: Checkout SwiftPackageXCFrameworkSample Repository
        uses: actions/checkout@v3

      - name: Set up environment variables
        run: |
          echo "CLONE_REPO_URL=https://github.com/NakaokaRei/CxxLibSample.git" >> $GITHUB_ENV
          echo "CLONE_REPO_DIR=CxxLibSample" >> $GITHUB_ENV
          echo "BUILD_SCRIPT=build_xcframework.sh" >> $GITHUB_ENV
          echo "XCFRAMEWORK_DIR=build/xcframework" >> $GITHUB_ENV
          echo "SWIFT_PACKAGE_DIR=." >> $GITHUB_ENV

      - name: Extract Tag Name
        id: extract_tag
        run: |
          TAG_REF="${{ github.event.client_payload.tag }}"  # "refs/tags/1.0.0"
          TAG_NAME="${TAG_REF#refs/tags/}"  # "1.0.0"
          echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT

      - name: Clone CxxLibSample Repository
        run: |
          git clone $CLONE_REPO_URL $CLONE_REPO_DIR

      - name: Make build_xcframework.sh executable
        run: chmod +x $CLONE_REPO_DIR/$BUILD_SCRIPT

      - name: Run build_xcframework.sh
        run: |
          cd $CLONE_REPO_DIR
          ./build_xcframework.sh

      - name: Verify XCFramework
        run: |
          if [ ! -d "$CLONE_REPO_DIR/$XCFRAMEWORK_DIR/CxxLibSample.xcframework" ]; then
            echo "Error: XCFramework not found at $CLONE_REPO_DIR/$XCFRAMEWORK_DIR/CxxLibSample.xcframework"
            exit 1
          fi

      - name: Archive XCFramework
        run: |
          TAG_NAME="${{ steps.extract_tag.outputs.tag_name }}"
          XCFRAMEWORK_PATH="$CLONE_REPO_DIR/$XCFRAMEWORK_DIR/CxxLibSample.xcframework"
          ZIP_PATH="build/CxxLibSample.xcframework-${TAG_NAME}.zip"
          mkdir -p build
          ditto -c -k --sequesterRsrc --keepParent "$XCFRAMEWORK_PATH" "$ZIP_PATH"

      - name: Upload XCFramework to GitHub Releases
        id: create_release
        uses: softprops/action-gh-release@v2
        with:
          files: build/CxxLibSample.xcframework-${{ steps.extract_tag.outputs.tag_name }}.zip
          tag_name: ${{ steps.extract_tag.outputs.tag_name }}
          name: ${{ steps.extract_tag.outputs.tag_name }}
          draft: true
          token: ${{ secrets.PAT_GITHUB }}

      - name: Get Release Asset URL and Checksum
        id: get_assets
        env:
          GH_TOKEN: ${{ secrets.PAT_GITHUB }} 
        run: |
          TAG_NAME="${{ steps.extract_tag.outputs.tag_name }}"
          ASSET_URL=$(echo '${{ steps.create_release.outputs.assets }}' | jq -r '.[] | select(.name == "CxxLibSample.xcframework-'${TAG_NAME}'.zip").url')
          echo "zipUrl=${ASSET_URL}" >> $GITHUB_ENV

          CHECKSUM=$(shasum -a 256 build/CxxLibSample.xcframework-${TAG_NAME}.zip | awk '{print $1}')
          echo "checksum=${CHECKSUM}" >> $GITHUB_ENV

          echo "zipUrl=${ASSET_URL}" >> $GITHUB_OUTPUT
          echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT

      - name: Update Package.swift
        run: |
          sed -i '' 's#let zipUrl = ".*"#let zipUrl = "${{ steps.get_assets.outputs.zipUrl }}.zip"#' Package.swift
          sed -i '' 's#let checksum = ".*"#let checksum = "${{ steps.get_assets.outputs.checksum }}"#' Package.swift

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.PAT_GITHUB }}
          branch: update-package-swift-${{ steps.extract_tag.outputs.tag_name }}
          commit-message: "chore: update Package.swift for version ${{ steps.extract_tag.outputs.tag_name }}"
          title: "chore: update Package.swift for version ${{ steps.extract_tag.outputs.tag_name }}"
          body: |
            This PR updates `Package.swift` to use version ${{ steps.extract_tag.outputs.tag_name }} of `CxxLibSample.xcframework`.
          base: master
          labels: 'release'
          add-paths: Package.swift
```

ワークフローが完了すると以下のようなPRが作成されるのでこれをマージして完了となります。

https://github.com/NakaokaRei/SwiftPackageXCFrameworkSample/pull/2


# 終わりに
本記事ではC++ライブラリをSwift Packageとして配布するための自動化方法について紹介しました。参考になれば幸いです。