こんにちは、LINE で LINE GAME Clinet SDK の開発をしている やまぐち です。 この記事は LINE Advent Calendar2016 の 10 日目の記事です。

弊社でも iOS / Android / Unity 向けに LINE GAME Clinet SDK を提供しています。コードは共通の C++ を使っているのですが、複数のプラットフォーム向けにビルドを行う必要があってソースファイルの管理やビルド方法がバラバラで大変ですよね。そこでクロスプラットフォーム向けのビルドシステムなどを調査してCMake を使ってクロスプラットフォームビルド環境を作ることにしました。
CMake を採用した理由
巷ではクロスプラットフォームでビルドをするための色々なソリューションがあります。ざっと上げてみると
などなど、他にも沢山あるかと思います。まず、前提として以下の要求を満たせるか、色々なソリューションを調査しました。
- ソースコードの一元管理が出来る。
- シェルなどからビルドが出来る。
- C++ ビルドはもちろん、Java、Objective-C などもビルドが出来る。
これを元に色々とベンチマークを取りました。その結果 CMake を採用することになりました。今回 CMake を採用した理由ですが
- C、C++、Java、Objective-C など複数の言語のビルドが可能。
- CMake は toolchain や custom target を使って柔軟にビルドが出来ます。
- サードパーティのライブラリが CMake のビルドをサポートしていることが多いので、ビルド連携がしやすい。
CMakeLists.txt
を自分のプロジェクトで読み込めば取り込むことが出来る!
- Xcode や Visual Studio のプロジェクトファイルも吐き出せる。
- IDE 環境が恋しくなってもこれで安心!
- 昔からあるから情報が多そう。
といった感じです。最後はかなり個人的な憶測が入っていますね。では実際に CMake を使ってクロスプラットフォームビルド環境を構築していきましょう。
CMake を使ったビルド
Toolchain ファイルの準備
CMake は標準で gcc、MSVC などに対応しています。標準的なコンパイラーでビルドするのであれば何も考えずにCMakeLists.txt
を作れば良いのですが Android などは専用のコンパイラーなどが用意されているので CMake から利用出来るようにするために Toolchain ファイルを作ります。これは CMake に対してコンパイラーはこれを使いますよー的なファイルでこれさえ作ればどんなコンパイラー環境にも対応出来るという訳ですね。
では早速、準備をしてみましょう。まずは github に Toolchain を公開してくれている人がいますのでそちらを使わせてもらいましょう。iOS の Toolchain はここに、 Android の Toolchain はここにあります。そのままでも十分に使えるので、最初はそのまま使って良いと思います。実際に構築した環境では上記の Toolchain をベースに clang でビルド出来るようにしたり c++_static を使えるようにしたりと若干のカスタマイズを行っています。
CMakeLists.txt の準備
Toolchain ファイルの準備も出来たところで、早速とビルドしてみましょう。 CMake ではソースを管理するのに CMakeLists.txt
というファイルが必要です。このファイルにどんなソースファイルをビルドするかといった情報を書いていくことになります。出来た CMakeLists.txt
をディレクトリのルートに置けば完了です。
一応、簡単なサンプルを置いておきます。これだけで実行ファイルが出来ちゃうんですよね、いやー簡単ですね!
cmake_minimum_required(VERSION 3.4.3)
project(sample)
add_executable(
sample
src/main.cpp
)
あとは CMake の実行ファイルにルートの CMakeLists.txt
を教えて上げれば CMake の設定に合わせて makefile が出来たり、Xcode のプロジェクトファイルが出力されるのでそれを使ってビルドするだけです。個人の好みに合わせて出力するのを変えればいいので、皆の好きな環境で出来ちゃいます。
実際にビルドしてみる
以下のようなサンプルを用意してみました。
.
├── CMakeLists.txt
└── src
└── main.cpp
では、実際にビルドしてみましょう。
cmake -DCMAKE_TOOLCHAIN_FILE="toolchainfile" .
make
-- Configuring done
-- Generating done
-- Build files have been written to: /sample
Scanning dependencies of target sample
[ 50%] Building CXX object CMakeFiles/sample.dir/src/main.cpp.o
[100%] Linking CXX executable sample
[100%] Built target sample
これだけでビルドができちゃいます!標準で makefile を出力するので、 Mac 環境であれば何も考えずにビルドが出来ます。 CMake はデフォルトでコンソール出力をカッコよく隠してしまうので詳細なログが欲しい場合は CMAKE_VERBOSE_MAKEFILE
変数を有効にすると良いかと思います。個人的にはカッコよく隠してもらった方が見やすいので、通常は CMAKE_VERBOSE_MAKEFILE
をOFF にして 詳細なログが必要になった時だけ ON にしています。ここらへんは好みの問題ですね。
さてビルドも出来たことで、CMake について少し深掘りしてみましょうか。
CMake のすごい所
マクロ機能が充実している
実際には以下のようなマクロを作って登録したりしています。どんなことをしているかと言うと ファイルをリストに追加するのと同時にディレクトリ名を使って source_group に登録しています。また Objective-C のコードをビルドするにはコンパイラーオプションに追加が必要なので *.m や *.mm ファイルに自動でオプションを付けています。別々に登録するのは面倒なので1回で済ませてしまおうという、手抜きの為ですね。もう少し最適化が出来るかもですが、一旦はこんな感じで。
macro(file_list VARIABLE_NAME)
set(TEMP_FILE_LIST ${ARGN})
foreach(ELEMENT IN LISTS TEMP_FILE_LIST)
if(${ELEMENT} MATCHES .mm$ OR ${ELEMENT} MATCHES .m$)
set_source_files_properties(
${ELEMENT}
PROPERTIES
COMPILE_FLAGS
"${CMAKE_OBJCXX_FLAGS}"
)
endif()
get_filename_component(ELEMENT_DIR ${ELEMENT} DIRECTORY)
if (NOT ${ELEMENT_DIR} STREQUAL "")
string(REPLACE ".." "" group_name ${ELEMENT_DIR})
string(REPLACE "/" "" group_name ${group_name})
source_group("${group_name}" FILES ${ELEMENT})
else()
source_group("" FILES ${ELEMENT})
endif()
endforeach()
list(APPEND ${VARIABLE_NAME} ${TEMP_FILE_LIST})
list(SORT ${VARIABLE_NAME})
unset(TEMP_FILE_LIST)
endmacro()
CMake は MACRO や FUNCTION で色々な表現が出来るのですごく便利です。CMake が標準で用意しているモジュールも CMake のマクロなどを駆使して作られているんですよね。いやー、よく出来ていますよね。
CMake を使い倒す
out-of-source ビルドのススメ
上記で試したサンプルもそうですが、巷で見かける CMake のサンプルも cmake .
となっているのをよく見かけると思います。これは in-source
ビルドと呼ばれる方法です。
in-source ビルドの問題点
in-source
ビルドをすると、作業ディレクトリに CMake が生成した中間ファイルなどが生成されます。SCM などでソースを管理していると非常に邪魔になりますよね。CMake が生成したファイルを消そうとした場合にも1つ1つファイルを選んで消さないといけないので非常に面倒くさいです。
実際に先程ビルドしてみたサンプルのディレクトリツリーを見てみると、以下のようになります。
.
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.6.3
│ │ ├── CMakeCCompiler.cmake
│ │ ├── CMakeCXXCompiler.cmake
│ │ └── CMakeSystem.cmake
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeOutput.log
│ ├── Makefile.cmake
│ ├── Makefile2
│ ├── TargetDirectories.txt
│ ├── cmake.check_cache
│ ├── progress.marks
│ └── sample.dir
│ ├── CXX.includecache
│ ├── DependInfo.cmake
│ ├── build.make
│ ├── cmake_clean.cmake
│ ├── depend.internal
│ ├── depend.make
│ ├── flags.make
│ ├── link.txt
│ ├── progress.make
│ └── src
│ └── main.cpp.o
├── CMakeLists.txt
├── Makefile
├── cmake_install.cmake
├── sample
└── src
└── main.cpp
元のディレクトリツリーは上記にありますが、かなり中間ファイルが増えていますよね。こうなるのを避ける為の仕組みが CMake にはあるので、それを見ていきましょう。
out-of-source ビルドって何?
out-of-source
ビルドは CMake が利用するワーキングディレクトリを指定してビルドする方法です。そうすることでビルドに必要なファイルなどが一箇所にまとまるので、削除も簡単です。また、Debug ビルドや Release ビルドでフォルダを分けることも出来るようになるので、良いことづくめという訳です。
どうやって使うの?
適当な作業ディレクトリを作り、そこで CMake コマンドを使って、CMakeLists.txt
があるディレクトリを指定するだけです。簡単ですね!では早速、out-of-source
ビルドしてみましょう。
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE="toolchain" ../
make
-- Configuring done
-- Generating done
-- Build files have been written to: /sample/build
Scanning dependencies of target sample
[ 50%] Building CXX object CMakeFiles/sample.dir/src/main.cpp.o
[100%] Linking CXX executable sample
[100%] Built target sample
特に出力された内容は代わり映えしないです。では out-of-source
ビルドされたディレクトリツリーを見てみましょう。
.
├── CMakeLists.txt
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.6.3
│ │ │ ├── CMakeCCompiler.cmake
│ │ │ ├── CMakeCXXCompiler.cmake
│ │ │ └── CMakeSystem.cmake
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── CMakeOutput.log
│ │ ├── Makefile.cmake
│ │ ├── Makefile2
│ │ ├── TargetDirectories.txt
│ │ ├── cmake.check_cache
│ │ ├── progress.marks
│ │ └── sample.dir
│ │ ├── CXX.includecache
│ │ ├── DependInfo.cmake
│ │ ├── build.make
│ │ ├── cmake_clean.cmake
│ │ ├── depend.internal
│ │ ├── depend.make
│ │ ├── flags.make
│ │ ├── link.txt
│ │ ├── progress.make
│ │ └── src
│ │ └── main.cpp.o
│ ├── Makefile
│ ├── cmake_install.cmake
│ └── sample
└── src
└── main.cpp
CMake 関連のファイルが build ディレクトリにまとまっていますね! in-source
と out-of-source
を見比べてみると一目瞭然だと思います。これならファイルを削除するのも簡単ですね。
上記ではプロジェクトの中に build ディレクトリを作っているので、厳密には out-of-source
ではないのですがニュアンスは伝わるかなと思います。
CMakeLists.txt が他のプロジェクトのサブプロジェクトとして使えるようにしよう
CMake の良いところで、他のプロジェクトの CMakeLists.txt
を読み込む(add_subdirectory
)ことで自分のプロジェクトに取り込む事ができます。この機能を最大限に活かす為には CMakeLists.txt
の書き方に注意をしないといけません。
実際に対応する前に CMake が用意している組み込み変数の一部を見ていきましょう。
CMAKE_SOURCE_DIR
- ソースツリーのトップディレクトリ(トップレベルの
CMakeLists.txt
があるディレクトリ)
- ソースツリーのトップディレクトリ(トップレベルの
CMAKE_BINARY_DIR
CMAKE_SOURCE_DIR
に対応するビルドディレクトリ
CMAKE_CURRENT_SOURCE_DIR
- 現在処理中の
CMakeLists.txt
があるディレクトリ
- 現在処理中の
CMAKE_CURRENT_BINARY_DIR
CMAKE_CURRENT_BINARY_DIR
に対応するビルドディレクトリ
PROJECT_SOURCE_DIR
- 現在のプロジェクトのトップディレクトリ(
project
コマンドを使用したCMakeLists.txt
があるディレクトリ)
- 現在のプロジェクトのトップディレクトリ(
PROJECT_BINARY_DIR
PROJECT_SOURCE_DIR
に対応するビルドディレクトリ
<name>_SOURCE_DIR
- name プロジェクトのトップディレクトリ(
project
コマンドを使用した<name>
プロジェクトのCMakeLists.txt
があるディレクトリ)
- name プロジェクトのトップディレクトリ(
<name>_BINARY_DIR
<name>_SOURCE_DIR
に対応するビルドディレクトリ
代表的なものを上げてみました。他にも沢山、組み込み変数はあるので、気になる方はこちらを参照してみてください。
サブプロジェクトとして取り込まないのであれば CMAKE_SOURCE_DIR
などを使えば良いのですがあるプロジェクトの CMakeLists.txt
がサブプロジェクトとして取り込まれた場合 CMAKE_SOURCE_DIR
を使っていると自分が思っていたパスとは違うパスが取得されてしまいます。これを避ける為に CMAKE_CURRENT_SOURCE_DIR
や <name>_SOURCE_DIR
などを使ってどこから add_subdirectory
をされても良いように CMakeLists.txt
を書きましょう。
こうすることで、他のプロジェクトでも、既存の CMakeLists.txt
を利用できるので連携が非常にやりやすくなります。巷にある CMake プロジェクトもそういう風になってくれると嬉しいのですが、そうなっていないことも多いです・・・。
実際に導入してみて
CMake を導入してみて iOS / Android / Unity (iOS/Android/Mac/Windows) のビルドが非常に簡単になりました。 CMakeLists.txt
にソースファイルを追加すれば済むので、毎回プロジェクトファイルに追加するという手間が省けて、大分効率が上がりました。 Java も CMake でビルドするようにしていますので、CMake で完結させることが出来ました。
ただ、CMake はリファレンスはしっかりと公式でサポートされているんですが、サンプルがないんですよね。変数やコマンドの意味は分かってもどうやって使うかというところに罠が潜んでました・・・。こういうときに github などを検索して、先駆者のコードを見れるのでそこから情報を収集したりしてました。いやー良い時代になったものです!
実際に構築をしてみましたが、まだまだ CMake のポテンシャルを引き出しきれていないのでもっと使い倒して行こうかと思っています。
CMake の 3.7 からは標準で Android の toolchain も組み込まれたみたいで今回やったようなことをしなくてもビルドが出来るようになっているようです。(まぁ実際にどう使うのか調べるのが大変なんですけれどもね!)
最後に
皆さんも CMake による快適なクロスプラットフォーム開発環境を作って開発効率をどんどん上げていってみてくださいね。
明日はKagayaさんによる「マイクロサービスのためのプロジェクト生成ツール Lazybones を使ってみた」です。お楽しみに!