はじめに
この記事ではCMakeで管理されているC++のライブラリをXCFrameworkにビルドし、Swift Pacakgeとして配布する一連の流れをGitHub Actionsで自動化するサンプルを紹介します。流れはざっくり以下です。
- C++ライブラリのリポジトリにタグを打つ
- 上記をトリガーにSwift Pacakge側のリポジトリのワークフローをトリガーする
- C++ライブラリをXCFrameworkにビルドする
- そのXCFrameworkをGitHub Release Assetsにアップロードする
- 上記のXCFrameworkを参照するようにPackage.swiftを更新しPRを作成する
最初からC++ライブラリにPackage.swiftをおけばいいのでは?
今回のようなシンプルな例だとそれでもいいのですが、実際のプロジェクトに後からPackage.swiftを置こうとすると、C++側の依存の解決やビルドが通らなかったり、色々めんどくさいことあったのでこのような構成になっています。
今回利用するサンプルコード
C++ライブラリ
構造は以下のようになっています。足し算をするadd
関数が存在するだけの簡単なプロジェクトです。タグが打たれるとSwift Package側にイベントをdispatchします。
.
├── .github
│ └── workflows
│ └── dispatch.yml
├── CMakeLists.txt
├── build_xcframework.sh
├── include
│ ├── module.modulemap
│ └── sample.h
└── source
└── sample.cpp
Swift Package
ビルドしたXCFrameworkはGitHub Release AssetsにアップロードされbinaryTargetで取得しています。また、Privateリポジトリにも対応できるようにURLはgithub.com
ではなくapi.github.com
としています。1
// 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します。
以下のようにタグがプッシュされた時に上記のAPIを叩きます。
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を生成する全体のスクリプトは以下から確認できます。
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
以降のステップになります。
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が作成されるのでこれをマージして完了となります。
終わりに
本記事ではC++ライブラリをSwift Packageとして配布するための自動化方法について紹介しました。参考になれば幸いです。