Skip to content

サードパーティライブラリがPodの依存ではなくソースコードとして内包されているが、ヘッダファイルがpublicになっていない #165

@kunichiko

Description

@kunichiko

表題の通り、DeviceConnect-iOS SDKを Podとして利用した場合、それに内包されている CocoaAsyncSocketのヘッダファイルがpublicになっていないため、アプリ等から利用することができません。

以下のアプリは、Swiftで書かれたアプリで、DeviceConnect-iOS SDKを CocoaPodsの Frameworkとして利用しています。
https://github.com/kunichiko/DeviceConnect-TestApp-iOS/tree/feature/issue-4-failure

アプリからは SDKが内包しているCocoaAsyncSocketが利用できないため、Podfile に以下のようにCocoaAsyncSocketへの依存を追加し、別途 CocoaAsyncSocketモジュールを取り込むようにしてみました。

target 'DCTestApp' do
  pod 'DeviceConnectSDK'
  pod 'DeviceConnectHostPlugin'

  pod 'CocoaAsyncSocket'
end

しかし、この状態で pod updateして Xcode (9.0)でビルドを行うと、DeviceConnect SDKフレームワークのビルド中に以下のようなエラーが発生してビルドが失敗します。

compileerror

原因を探ったところ、DeviceConnect SDK自体内包している CocoaAsyncSocketと、podが新たに取り込んだ CocoaAsyncSocketが衝突してしまっているように見えました。上記ブランチを見ていただくと、Xcodeのワークスペース内に GCDAsyncSocket.h ファイルが2つ存在する状態になっているのがわかるかと思います。

  • Pods/CocoaAsyncSocket/GCDAsyncSocket.h
  • Pods/DeviceConnectSDK/GCDAsyncSocket.h

本来であれば、DeviceConenctSDKモジュールをビルドする際は、後者のヘッダファイルがヘッダーサーチパス内に存在するはずです。しかし、同名のファイルが2箇所にあることで、CocoaPods、あるいはXcodeのいずれかが誤動作し、GCDAsyncSocket.hがヘッダーサーチパス内に無い状態で DeviceConenctSDKモジュールをビルドしようとし、問題が発生しているようです。

おそらく、CocoaAsyncSocketだけでなく、他のモジュール(CocoaHTTPServer, RoutingHTTPServerなど)でも同様の問題が発生すると考えられます。

解決方法1

解決方法として、DeviceConnectSDKが内包している CocoaAsyncSocketのヘッダファイルを publicにし、アプリ等から利用できるようにする方法が考えられます。

現状の Pod specを見ますと、以下のように、source_filesには Dependenciesフォルダ配下のソースコードが含まれていますが、public_header_filesに Dependencies が含まれていないため、モジュールの外部から利用することができません。

    s.public_header_files = base_path + "/DConnectSDK/DConnectSDK/*.h"
    s.source_files = base_path + "/DConnectSDK/DConnectSDK/*.h", base_path + "/DConnectSDK/{Classes,Dependencies}/**/*.{h,m,c}"

このpublic_header_filesに Dependencies を含めていただければ、DeviceConnect SDKに内包された CocoaAsyncSocketが利用できるようになります。

実際にそれを試した PodSpecを、私が forkした DeviceConnect-PodSpecリポジトリのバージョン2.1.1として置いてあります。
https://github.com/kunichiko/DeviceConnect-PodSpecs/tree/master/Specs/DeviceConnectSDK/2.1.1

例えば Swiftの場合は、import DConnectSDK で DeviceConnect SDKをインポートすれば、GCDAsyncSocketクラスが利用できるようになります。本来であれば import CocoaAsyncSocket と書くべきところですのであまりスマートではありませんが、ひとまず問題は回避できます。

また、このように GCDAsyncSocket.h を SDKの public headerにしてしまえば、Thetaプラグインなどに GCDAsyncSocket.hのコピーを持つ必要も無くなるのではと思います。(後述の「補足」参照)

解決方法2

CocoaPodsの流儀に従うのであれば、Dependenciesにある以下のモジュールをソースコードとして取り込まずに、Podの依存モジュールとして解決する方がスマートかと思います。

これを実際に行ったサンプルアプリを feature/issue-4-resolved ブランチに置いてあります。
https://github.com/kunichiko/DeviceConnect-TestApp-iOS/tree/feature/issue-4-resolved

こちらのブランチでは、私が forkした DeviceConnect-PodSpecリポジトリで定義されている DConnectSDK 2.1.2を参照しています。2.1.2では、以下のようにして Dependencies配下のモジュールを取り込まず、Podの依存で解決するようにしてあります。

https://github.com/kunichiko/DeviceConnect-PodSpecs/tree/master/Specs/DeviceConnectSDK/2.1.2

# Dependenciesフォルダ以下のソースのうち、Podに無い GCIPUtilとmultipart-parser-cだけを source_filesに入れる
s.source_files = base_path + "/DConnectSDK/DConnectSDK/*.h", base_path + "/DConnectSDK/Classes/**/*.{h,m,c}", base_path + "/DConnectSDK/Dependencies/GCIPUtil*.{h,m,c}", base_path + "/DConnectSDK/Dependencies/multipart-parser-c/*.{h,m,c}"

# 以下のモジュールはPodの依存として宣言
s.dependency 'CocoaAsyncSocket', '~> 7.6.1'
s.dependency 'CocoaHTTPServer', '~> 2.3'
s.dependency 'CocoaLumberjack', '~> 3.3.0'
s.dependency 'RoutingHTTPServer', '~> 1.0.0'

ただ、 このままですと CocoaLumberjackのマクロがエラーになる問題 などが発生してしまうため、Podfileに以下のようなワークアラウンドを入れる必要がありました。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'DD_LEGACY_MACROS=1']
      config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO'
    end
  end
end

まとめ

私の方では上記2つの解決方法を見つけましたが、いかがでしょうか。もし、ほかに良い方法があるようでしたら、ご教授いただけますと助かります。

なお、いずれの場合でも、Theta Pluginや ChromeCast Pluginなどのように GCDAsyncSocket.h を持ってしまっている podを参照すると同じような問題が発生する可能性があります。私の方ではプロジェクトの全体構成が見えていないところもありますので、どのような解決方法が良いか、ご判断いただけますと幸いです。

補足

今回はアプリ内で CocoaAsyncSocketを利用するケースを提示いたしましたが、Device Plugin内でCocoaAsyncSocketを利用する(Podの依存がある)場合も同じ問題が起こります。

CocoaAsyncSocketを利用している Thetaプラグインでは問題が起こっておりませんが、こちらのプロジェクトを拝見したところ、Podの依存は作らずに、プロジェクト内に GCDAsyncSocket.hのコピーを取り込むことで、DeviceConnectSDK内にある実装(GCDAsyncSocket.m)を呼び出せるようにしていらっしゃるようです。
この考え方を応用すると、アプリ内にGCDAsyncSocket.hのコピーを持ち、Bridging Headerに GCDAsyncSocket.h を入れることで解決する方法もありそうです。

しかし、デバイスプラグインをSwiftで作る場合は Bridging Headerが使えないため、そういった解決方法は使えません。そのため、解決方法1、2のいずれかを行う必要があると考えております。
(2017.10.16追記) Bridging Headerは使えませんでしたが、フレームワークの Umbrella Headerに追加することで同様のことは可能そうです。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions