LINE Engineering
Blog

CMake を使ったクロスプラットフォーム開発環境

LINE 2016.12.14

LINE Dev

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

cmake_logo

弊社でも 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-sourceout-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>_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 を使ってみた」です。お楽しみに!

AdventCalendar CMake

LINE 2016.12.14

LINE Dev

Add this entry to Hatena bookmark

リストへ戻る