Swift PackageでC/C++コードだけのtargetからxcframeworkを作る
私が作っているUnrar.swiftというRAR書庫の展開する機能を提供するSwift Package Manager形式のライブラリがあります。
ある日、このライブラリに「Swift Playgroundsで動かしたいのだけれどC++のコンパイラがない (iPadってそうなんですね) ので、プリコンパイルバイナリで配布してくれたらうれしい」というissueがたてられました。
Include C parts as a pre-compiled library · Issue #4 · mtgto/Unrar.swift · GitHub
たしかにXcode 12からSwift Packageでもプリコンパイル形式に対応しているようですし (Distributing Binary Frameworks as Swift Packages) ゴールデンウィークを使って挑戦してみました。
もっと簡単にできないか試行錯誤したんですが、結局Package.swiftから swift package generate-xcodeproj
でxcodeprojファイルを生成し、手動でxcodeprojの設定を変更しxcodebuildでframework生成&xcframework生成することでなんとかなりました。
対象のUnrar.swiftはunrarのC/C++コードを利用しています。そのためSwift PackageではunrarのC/C++部分のコードをもつtarget "Cunrar" と、Swiftから利用可能にするためのインターフェイスを提供するtarget "Unrar" の二つを持っています。
Swiftから直接C++を呼び出すことはできませんが、unrarではCのインターフェイスが提供されているためその部分を "unrar.h" に定義しています。詳しくはGitHubのソースコードを参照してください。
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "Unrar",
products: [
.library(
name: "Unrar",
targets: ["Unrar"])
],
targets: [
.target(
name: "Unrar",
dependencies: ["Cunrar"]),
.target(
name: "Cunrar",
exclude: [ ... ],
sources: [ ... ],
cSettings: [ ... ]),
.testTarget(
name: "UnrarTests",
dependencies: ["Unrar"],
resources: [.process("fixture")]),
]
)
今回のゴールはPackage.swiftでtarget "Cunrar" を binaryTargetに変更し、Swift Playgroundでlibrary "Unrar" が利用できるようにすることです。
Package.swiftでbinaryTargetに指定するものは複数のframeworkをまとめたXCFrameworkへのパス、もしくはそれをzipでまとめたもののURLです。今回はインターネットで配布したいのでXCFrameworkをzipにしてGitHubのリリースタグにzipファイルを配置することにします。
調査した環境
- macOS 12.3.1 on MacBook Air (M1, 2020)
- Xcode 13.3.1
swift-create-xcframeworkを使う (失敗)
unsignedapps/swift-create-xcframeworkというツールがあります。これはSwift Packageのルートディレクトリで swift create-xcframework
コマンドを実行することでXCFrameworksをビルドしてくれる便利なツールです。READMEによると内部でXcodeプロジェクトの作成とxcodebuildでのXCFrameworks作成を自動でやってくれるそうです。
Swift PackageはC/C++ソースのtargetも指定することができ、実際swift-create-xcframeworkでもCunrarのxcframeworkを作ることはできます。
$ swift create-xcframework --platform macos Cunrar
(iPadのSwift Playgroundで試す前に、ひとまずmacOS上で動くかどうかの確認)
xcodebuildが裏で走り、"Cunrar.xcframework" がカレントディレクトリに生成されました。
$ file Cunrar.xcframework/macos-arm64_x86_64/Cunrar.framework/Cunrar
Cunrar.xcframework/macos-arm64_x86_64/Cunrar.framework/Cunrar: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64
- Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64
- Mach-O 64-bit dynamically linked shared library arm64]
Cunrar.xcframework/macos-arm64_x86_64/Cunrar.framework/Cunrar (for architecture x86_64): Mach-O 64-bit dynamically linked shared library x86_64
Cunrar.xcframework/macos-arm64_x86_64/Cunrar.framework/Cunrar (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64
ちゃんとIntel/Apple SiliconのUniversal Binaryになっているようです。
別のSwift Package "ExampleUseCunrar" を作り、そこからCunrar.xcframeworkをbinaryTargetとして追加してみます。
// swift-tools-version: 5.6
import PackageDescription
let package = Package(
name: "ExampleUseCunrar",
dependencies: [],
targets: [
.target(
name: "ExampleUseCunrar",
dependencies: ["Cunrar"]),
.binaryTarget(
name: "Cunrar",
path: "/path/to/Cunrar.xcframework"
)
]
)
import Cunrar
print("\(ERAR_SUCCESS)")
この状態でコンパイルしてみるとCunrarモジュールがないと言われてしまいました。どうやらCunrar.xcframeworkはライブラリファイル自体はもっているもののSwiftモジュールをもっていないと認識されビルドにこけたようです。
$ swift build
Building for debugging...
/path/to/ExampleUseCunrar/Sources/ExampleUseCunrar/main.swift:1:8: error: no such module 'Cunrar'
import Cunrar
^
/path/to/ExampleUseCunrar/Sources/ExampleUseCunrar/main.swift:1:8: error: no such module 'Cunrar'
import Cunrar
Swift Packageからxcodeprojを生成、xcodebuildでframeworkを作成→frameworkからxcframeworkを作成 (とりあえずmacOSだけ)
XCFrameworkを他のSwiftプロジェクトから参照するにはSwiftモジュールとしてFrameworkを生成する必要があることがわかりました。
自作のmodulemapを組み込んだFrameworkの作り方 を参考にmodulemapファイルを作成し、Frameworkを作る、という手順を踏めばよさそうです。
最終的にはiOS Playgroundsで動くようなxcframeworkを生成しますが、まずは簡単に動作確認ができるmacOS用のxcframeworkを作成します。
xcframeworkやその中身であるframeworkを作成するためにはxcodebuildコマンドを利用します[1]。
1. modulemapファイルの作成
modulemapの書き方は ClangのModulesドキュメント を参考にしつつエラーが出ないかどうかを試行錯誤しました。
framework module Cunrar {
umbrella header "unrar.h"
export *
}
大事なところとして、umbrella header "unrar.h"
のように"unrar.h"はmodulemapファイルからの相対パスにする必要はありません。相対パスで書いてしまうとFramework内に配置されるパスがHeadersとModulesで別のところになるため "unrar.hが見つからない" というコンパイルエラーが発生することになります。
2. Package.swiftからxcodeprojファイルを生成しプロジェクト設定を修正
swift package generate-xcodeproj
を実行することで Package.swift からXcodeプロジェクトファイルを生成します。このままxcodebuildでFrameworkを生成するとSwiftモジュールにならないため、以下の修正を行います。
Build Settings - Build Options の修正
- Build Libraries for Distribution (BUILD_LIBRARY_FOR_DISTRIBUTION) をNOからYESに変更
Build Settings - Packaging の修正
- Defines Module (DEFINES_MODULE) をNOからYESに変更
- Module Map File (MODULEMAP_FILE) に "Sources/Cunrar/module.modulemap" を指定 ($SRCROOTからの相対パス)
Build Phases の修正
現状 (Xcode 12.3.1) ではPackage.swiftでtargetのpublicHeadersPathが指定されていてもgenerate-xcodeprojで生成されるxcodeprojではHeaderコピーのBuild Phaseが設定されないようです。
なのでNew Headers Phaseを追加し、Sources/Cunrar/include/unrar.h を必ず Public に追加します。
※これをやらないとframeworkは生成できても "unrar.h" が梱包されず、Cunrar.xcframeworkを使用するプログラムのビルド時に
.build/arm64-apple-macosx/debug/Cunrar.framework/Modules/module.modulemap:2:20: error: umbrella header 'unrar.h' not found
main.swift:1:8: error: could not build Objective-C module 'Cunrar'
のようにコンパイルエラーが発生します。
今回は必要ないですが、別のtargetに依存したtargetのframeworkを作る場合には、generate-xcodeprojで生成されるxcodeprojは依存先Frameworkをコピーする設定になっていないのでCopy Filesフェーズを追加してあげる必要があります。
3. XCFrameworkをビルドする
上記1, 2の修正をしたxcodeprojを元に、xcodebuildでXCFrameworkをビルドします。
$ xcodebuild -sdk macosx -target Cunrar build
(中略)
Touch /path/to/Unrar.swift/build/Release/Cunrar.framework (in target 'Cunrar' from project 'Unrar')
cd /path/to/Unrar.swift
/usr/bin/touch -c /path/to/Unrar.swift/build/Release/Cunrar.framework
** BUILD SUCCEEDED **
$ xcodebuild -create-xcframework -output Cunrar.xcframework -framework build/Release/Cunrar.framework
これで生成したCunrar.xcframeworkであれば、binaryTargetとしてちゃんと利用できます。
$ swift run
Building for debugging...
[3/3] Linking ExampleUseCunrar
Build complete! (0.25s)
0
Swift Packageからxcodeprojを生成、xcodebuildでframeworkを作成→frameworkからxcframeworkを作成 (macOS & iOS)
あとはxcodebuildでiOS, iPhone Simulator用のframeworkも生成し、xcframeworkとしてまとめてしまえばOKです。
簡単なMakefileを作りました。
TARGET=Cunrar.xcframework
ZIP=Cunrar.zip
FRAMEWORK_MACOS=build/Release/Cunrar.framework
FRAMEWORK_IPHONEOS=build/Release-iphoneos/Cunrar.framework
FRAMEWORK_IPHONESIMULATOR=build/Release-iphonesimulator/Cunrar.framework
all: $(TARGET)
clean:
rm -rf $(TARGET) $(FRAMEWORK_MACOS) $(FRAMEWORK_IPHONEOS) $(FRAMEWORK_IPHONESIMULATOR)
$(ZIP): $(TARGET)
zip -r $@ $<
shasum -a 256 $@
$(TARGET): $(FRAMEWORK_MACOS) $(FRAMEWORK_IPHONEOS) $(FRAMEWORK_IPHONESIMULATOR)
xcodebuild -create-xcframework -output $@ -framework $(FRAMEWORK_MACOS) -framework $(FRAMEWORK_IPHONEOS) -framework $(FRAMEWORK_IPHONESIMULATOR)
$(FRAMEWORK_MACOS):
xcodebuild -sdk macosx -target Cunrar build
$(FRAMEWORK_IPHONEOS):
xcodebuild -sdk iphoneos -target Cunrar build
$(FRAMEWORK_IPHONESIMULATOR):
xcodebuild -sdk iphonesimulator -target Cunrar build
binaryTargetとしてリモートのファイルを使うためにはxcframeworkをzip書庫にしてアップロードしてあげればOKです (くわしくはAppleのドキュメント参照)。
おわり
ここまでの処理をやることで別のSwift PackageからbinaryTarget形式でのCunrarを利用できるようになりました。iPadを持ってないためSwift Playgrouds 4で利用できるようになったのかわからないのですが、一旦GitHub Releasesページにxcframeworkをzipにしたものを置いて試してもらうことにしました。
調べてみてわかったこととして、frameworkやxcframeworkを作るには現状だとxcodebuild前提の処理が多いということでした。swift packageコマンドだけで解決すればとても楽になるので早くそういうサポートが来てほしいですね [2]。
参考にしたもの
- https://github.com/unsignedapps/swift-create-xcframework
-
OpenCV を XCFramework にして Swift Package Manager 経由で iOS で使ってみた
- OpenCVのビルドスクリプトではヘッダファイルをframework内にコピーしていました。おそらく生成されたxcodeprojでは自動でヘッダーファイルを設置してくれないからなのかなと予想しています。
- 自作のmodulemapを組み込んだFrameworkの作り方
-
Create an XCFramework にあるようにxcodebuildによるxcframework作成にはframeworkを指定する方法とライブラリファイルとヘッダーファイルを指定する方法があります。今回はframeworkを指定する方法を選択していますがライブラリ&ヘッダーからでもSwift Moduleであるxcframeworkを作成することができます。 ↩︎
-
swift package generate-xcodeprojを実行すると "warning: Xcode can open and build Swift Packages directly. 'generate-xcodeproj' is no longer needed and will be deprecated soon." と怒られるのでxcodeproj生成ができなくなる前にはframework作成の方法ができるといいなと思います。 ↩︎
Author And Source
この問題について(Swift PackageでC/C++コードだけのtargetからxcframeworkを作る), 我々は、より多くの情報をここで見つけました https://zenn.dev/mtgto/articles/952023a7cddaa6著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol