はじめに
本文書は、macOSのコマンドラインアプリケーションを、Swiftで開発する場合の課題とその解決方法について説明しています。
- 2018.09.23 追記: Xcode 10.0では下記の手順が実施できな事が分かりました。文末に状況を記載します。
環境
本文書の想定する開発環境/言語は以下の通りです:
* IDE: Xcode version 9.2
* Language: Swift4
* macOS: High Sierra (10.13.2)
課題1
Xcodeを使って、Swiftで"Console application"をビルドする時に、カスタムフレームワークをリンクすると、次のエラーが発生します(エラーメッセージ中のCanary
はリンクしようとするカスタムフレームワーク)の名前です):
dyld: Library not loaded: @rpath/libswiftAppKit.dylib
Referenced from: .../DerivedData/Canary-btnobogqbomcotbiqzxllcmkuijv/Build/Products/Debug/Canary.framework/Versions/A/Canary
Reason: image not found
課題1の発生原因
コンソールアプリケーションは、単一のバイナリで構成されるために、動的リンクするためのカスタムフレームワークを持つことができません。このために、上のリンクエラーが発生します。
課題1の解決方法
アプリケーションにバンドル情報を持たせます。本文書で想定する構造を以下に挙げます:
- インストールディレクトリ:
$(HOME)/tools/JSRunner
- 実行ファイル:
$(HOME)/tools/JSRunner/jsrunner
- 関連フレームワーク:
$(HOME)/Tools/JSRunner/JSRunner.bundle/Contents/Framework/
JSRunner
はアプリケーションを格納するディレクトリ、jsrunner
は実行ファイル本体、JSRunner.bundle
がバンドル情報を格納するディレクトリです。
バンドル(bundle)のビルド
説明
参照するカスタムフレームワークと標準ライブラリを格納するためのバンドル構造をビルドします。新しいターゲットとして、"Bundle"を選択します。
ターゲットの名前は、"ツール名 + Bundle"などどして、ツール自体をビルドするターゲットとの名前の衝突を避けなければなりませんが、ビルド設定中のProductの名前(PRODUCT_NAME
)はツール名と同じにしてください。
Build setting
の設定
-
Build Options
-
Always Embed Swift Standard Library
にYes
を設定
-
-
Deployment
-
Installation Directory
に$(HOME)/Tools/JSRunner
を設定 -
Skip Install
にNo
を設定
-
-
Packaging
-
Product Name
をアプリケーション名(JSRunner)に変更
-
-
Build Phase
の設定 (カスタムフレームワークの組み込み)- 左上の"+"ボタンを押して
New Copy Files Phase
を追加します - Destinationを"Framework"とし、コピーするフレームワーク(複数)をリストに追加します
-
Copy Items If needed
を非選択状態にします。Added folder
の設定は、Create Folder reference
としておきます。これによりカスタムフレームワークに関して、コピーではなくオリジナルを参照できます。
- 左上の"+"ボタンを押して
アプリケーションの設定変更
アプリケーションが、前述のバンドルを参照する様に変更します。
-
Build settings
のFramework search path
の先頭に、$(TARGET_BUILD_DIR)/$(PRODUCT_NAME).bundle/Contents/Frameworks
を追加します。このディレクトリにはバンドル内のフレームワーク(のコピー)がおかれています。 -
Build settings
のRunpath search paths
に@executable_path/$(PRODUCT_NAME).bundle/Contents/Frameworks
を設定します。実行時のフレームワークの検索には上のパスが使用されます。
ここまでの設定で、カスタムフレームワークがアプリケーションにリンクできる様になりました。
課題2
これまでの設定にてアプリケーションを実行すると、下記の警告が表示される様になります。このメッセージは、アプリケーションが参照している標準ライブラリと、カスタムフレームワークが参照しているそれの実態が異なる事に関する警告です。
objc[10315]: Class _TtC8Dispatch16DispatchWorkItem is implemented in both .../build/DerivedData/Canary-btnobogqbomcotbiqzxllcmkuijv/Build/Products/Debug/UnitTest.bundle/Contents/Frameworks/libswiftDispatch.dylib (0x1017d5530) and .../build/DerivedData/Canary-btnobogqbomcotbiqzxllcmkuijv/Build/Products/Debug/UnitTest (0x1005e4230). One of the two will be used. Which one is undefined.
課題2の発生原因
プログラミング言語がObjective-CからSwift変わる際に、標準ライブラリの取り扱いが変わりました。
- Objective-Cの場合、標準ライブラリはOSに含まれていて、アプリはそのライブラリを使用します。
- これに対して、Swiftのアプリケーションは、個別に標準ライブラリのコピーを持ちます。これは標準ライブラリのAPI/ABIが随時変化しており、互換性を保てないためです。
コマンドラインツールに標準ライブラリを組み込むために、Xcodeは次のコンパイルオプションを準備しています:
Always Embed Swift Standard Libraries: YES (or No)
しかし、この方法ではフレームワークを使うことができません(フレームワークがこの標準ライブラリとリンクできないためです)。よって、上のバンドルに標準ライブラリ(のコピー)を組み込んだのですが、これとアプリケーションが参照する標準ライブラリが別ファイル(中身は同じ)となるのが、上の警告が発生する原因です。
課題2の解決方法
アプリケーションが参照するライブラリの検索パスに、バンドル内のライブラリの置き場所を設定します。
* Build settings
のLibrary search paths
に$(TARGET_BUILD_DIR)/JSRunner.bundle/Contents/Frameworks
を設定します
これによって、フレームワークとアプリケーションが同一のライブラリファイルを参照することになり、課題2の警告はなくなるはずです。
まとめ
上に述べた手順で、Swiftでコンソールアプリが作れる様になります。私自身は、自作フレームワークの単体テストにて、コンソールアプリを採用することが多いです。
Xcode10.0について
Xcode10.0では、上で述べた「バンドル(bundle)のビルド」が失敗するようになりました。
error: Build input file cannot be found: '..../build/DerivedData/JSTools-bazqhvlmezerpyhbnbzazxroewte/Build/Products/Debug/jstools.bundle/Contents/MacOS/jstools'
CopySwiftLibs
なるコマンドでエラーが発生している様です。原因は、Xcode 10で、ビルドシステムが刷新されたからの様です。Xcode 9から組み込まれていた機能(ただしデフォルトではない)の様です。
ビルドシステムをもとに戻すと、エラーはなくなりました。ビルドシステムの切り替えは、File
メニュー、Project Setting ...
メニューのBuild System
メニューにて切り替える事ができます。