[Swift] コンソールアプリケーションでのカスタムフレームワークの使用方法

# はじめに
本文書は、macOSのコマンドラインアプリケーションを、Swiftで開発する場合の課題とその解決方法について説明しています。

* 2018.09.23 追記: Xcode 10.0では下記の手順が実施できな事が分かりました。文末に状況を記載します。

# 環境
本文書の想定する開発環境/言語は以下の通りです:
* IDE: [Xcode version 9.2](https://developer.apple.com/xcode/ Xcode9)
* Language: [Swift4](https://developer.apple.com/swift/ 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`の設定 (カスタムフレームワークの組み込み）
 1. 左上の"+"ボタンを押して`New Copy Files Phase`を追加します
 2. Destinationを"Framework"とし、コピーするフレームワーク(複数)をリストに追加します
 3. `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で、[ビルドシステムが刷新された](https://developer.apple.com/xcode/whats-new/)からの様です。Xcode 9から組み込まれていた機能(ただしデフォルトではない）の様です。

ビルドシステムをもとに戻すと、エラーはなくなりました。ビルドシステムの切り替えは、`File`メニュー、`Project Setting ...`メニューの`Build System`メニューにて切り替える事ができます。