こんにちは。田原です。
今回は良く使うCMakeコマンドの解説を予定していましたが、予定を変更してスクリプト・モードと並ぶCMakeの難関Multiple Configurelationについて解説したいと思います。CMakeはMakeなどのシングル・コンフィグレーションとVisual Studioなどのマルチ・コンフィグレーションの2種類をサポートしています。この2つも似てて非なるものなのでかなりハマりました。こちらも早目に説明した方が良さそうです。
1.シングル・コンフィグレーションとマルチ・コンフィグレーション
ぶっちゃけDebugビルドとReleaseビルドの2つのビルド・モード(*1)
をどのようにサポートするのか?という話です。シングル・コンフィグレーションは、ビルド・システム(≒Makefile)を生成する際にDebug・Releaseを指定します。マルチ・コンフィグレーションは、ビルド・システム(≒*.sln)を生成する際にはDebug・Releaseを指定せず、ビルドする時に指定します。
前者はたいへん古くからあるMakeが採用しています。実装の数が多いです。
後者は、マイクロソフトのVisual Studioで使われている MSBuild が採用しています。こちらを採用しているツールの数は少なく、私はVisual Studioしか使っていません。しかし、Visual Studioはユーザ数が多いので無視できるものではないです。(GENERATOR_IS_MULTI_CONFIGによると、Xcodeもマルチ・コンフィグレーションだそうです。)
ですので、マルチ・プラットフォーム対応する際には、この両者への対応は事実上必須と思います。それにはCMakeが圧倒的にメジャーです。(他にもシングル・コンフィグレーションとマルチ・コンフィグレーションの両方に対応するビルド・ツールは存在する可能性はありますが、私はこれしか知らないです。)
(*1)2つのビルド・モード`
実際には CMake はデフォルトで4つのビルド・モードをサポートしています。残り2つは RelWithDebInfo と MinSizeRelです。
RelWithDebInfo はデバッグ情報付きのリリース・ビルド、MinSizeRelはサイズ優先のリリース・ビルドです。私は今の所使ったことはないのですが、第28回 マルチプラットフォームなスタック・トレースのようにリリース・ビルドでもデバッグシンボルを一時的に付けたいような時に有用だろうと思います。
2.シングル・コンフィグレーション
元々ビルド・システムは Make で始まりましたので、サポートしている処理系は多いです。マイクロソフトも実は NMake という Make を提供しています。
Makefile Generators と Ninja がシングル・コンフィグレーションです。
Makefile Generators
- Borland Makefiles
- MSYS Makefiles
- MinGW Makefiles
- NMake Makefiles
- NMake Makefiles JOM
- Unix Makefiles
- Watcom WMake
Ninja Generator
シングル・コンフィグレーションの場合、CMakeコマンドでビルド・システムを生成する際にCMAKE_BUILD_TYPEシンボルを定義してビルド・モードを指定します。
> cmake .. -DCMAKE_BUILD_TYPE=Debug
CMAKE_BUILD_TYPEを指定しなかった時
処理系によって振る舞いが異なりました。gcc(linux)とMinGW(windows)はDebugビルドでもReleaseビルドでもありませんでした。
NMakeは-DCMAKE_BUILD_TYPE=Debugと同じ結果となりました。詳細後述します。
3.マルチ・コンフィグレーション
Visual Studioをお使いの方はご存知と思いますが、Visual Studioではプロジェクトを生成後、「ビルド(B) → 構成マネージャー(O)」で開く下記ダイアログの「アクティブソリューション構成(C)」で設定します。
CMakeで生成したソリューション(*.sln)
の場合、Debug、Release、RelWithDebInfo、MinSizeRelを選択できます。それぞれの意味はシングル・コンフィグレーションの場合と同じです。
シングル・コンフィグレーションでは、ビルド・システム(≒Makefile)生成時に指定しましたが、マルチ・コンフィグレーションではビルド・システム(≒*.sln
)生成時ではなく、ビルド時に指定します。つまり、1つのビルド・システムで複数のビルド・モード(構成)をサポートするので、マルチ・コンフィグレーション(複数の構成)と呼ばれています。
4.CMakeLists.txtの指定方法の相違
CMakeLists.txtでは、それぞれのビルド・モードに対してコンパイル・オプションやその他非常に数多くの様々な条件を設定することができます。シングル・コンフィグレーションとマルチ・コンフィグレーションの両方に通用するように設定するにはコツが必要です。
ここでは、コンパイル・オプションをビルド・モードによって切り替えることは比較的多いと思いますので、コンパイル・オプションを例にとって解説します。
話を単純にするため、-D
や/D
オプションでプリプロセッサ・シンボル(マクロ)を指定します。BUILD_TYPEシンボルをCMakeLists.txtで設定し、それを-D
オプションで渡して表示するプログラムを作成します。
まず、プログラムです。単純に文字列が定義されたBUILD_TYPEシンボルの内容を出力しています。(もし、BUILD_TYPEが定義されていなければ"<None>"
を表示します。)
#include <iostream> #if !defined(BUILD_TYPE) #define BUILD_TYPE "<None>" #endif int main() { std::cout << "Hello, World!!\n"; std::cout << "BUILD_TYPE=" BUILD_TYPE "\n"; return 0; }
4-1.シングル・コンフィグレーションの場合
シングル・コンフィグレーション(NMakeを使いますが、gccやMinGWでも同様です)の場合、次のようなCMakeLists.txtを使ってコンパイル・オプションを切り替えるようとするのではないでしょうか? 私はそうでした。(そしてハマりました。)
CMAKE_BUILD_TYPE変数の値をチェックしてコンパイル・オプションを切り替え、BUILD_TYPEシンボルへの設定を切り替えてます。
cmake_minimum_required(VERSION 3.5.1) project(hello) if(MSVC) string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc") if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DBUILD_TYPE=\\\"Debug\\\"") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DBUILD_TYPE=\\\"Release\\\"") endif() else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11") if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBUILD_TYPE=\\\"Debug\\\"") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBUILD_TYPE=\\\"BUILD_TYPE=Release\\\"") endif() endif() add_executable(hello hello.cpp)
cmake_minimum_requiredについて
これは使用するCMakeの最低バージョンを指定します。これより古いCMakeで生成しようとするとエラーがでます。
もう誰も覚えていないような気がしますが、当講座は当初に linuxはUbuntsu 16.4 LTC にて進めると宣言しています。これに標準搭載されている CMake は 3.5.1 なので、ここでは3.5.1を指定しているだけです。上記のCMakeLists.txtはもっと古いCMakeでも問題ないと思います。
しかし、問題のないCMakeバージョンを探し回るのは大変ですので、私は使用するCMakeのバージョンを決めたら、そのバージョンを指定するようにしています。
string(REPLACE …)について
これは文字列に含まれる指定した文字列を置換するコマンドです。”/W4″オプションを指定したいのですが、NMake用に生成する時はCMakeはデフォルトで”/W3″オプションをCMAKE_CXX_FLAGS に設定します。2つの”/W3″と”/W4″を指定するとVC++が警告を表示するので、このstringコマンドで”/W3″を””へ置換することで消しています。
/DBUILD_TYPE=\\\”Debug\\\”の \\\ について
\を3つ連ねているのはTypoではありません。最初にsetコマンド解釈時に \によるエスケープが1回づつ効いて、CMAKE_CXX_FLAGSに設定される文字列が /DBUILD_TYPE=\”Debug\” となります。(\\→\ 、\”→”と展開)そして、Makefileをを使ってビルドする際にもう一度エスケープが展開されて、
/DBUILD_TYPE=”Debug”` となり、BUILD_TYPEマクロに “Debug” が設定されるのです。
Visual Studio付属のNMakeを使う場合は、スタート・メニューから Developer Command Prompt for VS 2017 で起動したコマンド・プロンプトを使ってCMakeLists.txtとhello.cppを置いているフォルダへ移動して下さい。(なお、ubuntuでも端末を使って同様な操作が可能です。結果も同様です。)
> mkdir build-debug > cd build-debug > cmake -G "NMake Makefiles" .. -DCMAKE_BUILD_TYPE=Debug (生成状況略) > nmake (ビルド状況略) > hello.exe Hello, World!! BUILD_TYPE=Debug > cd .. > mkdir debug-release > cd debug-release > cmake -G "NMake Makefiles" .. -DCMAKE_BUILD_TYPE=Release (生成状況略) > nmake (ビルド状況略) > hello.exe Hello, World!! BUILD_TYPE=Release
指定したビルド・モード通りにコンパイル・オプションが定義され、その内容が表示されました。期待通りに動作しています。
4-2.マルチ・コンフィグレーションの場合
NMakeの場合、Visual Studio用のソリューション(*.sln)
が生成されません。Visual Studioはマルチ・コンフィグレーションなので如何ともし難い部分です。しかし、デバッグ時はできるだけVisual Studioを使いたいのでソリューション(*.sln)
を生成させたいです。
実は、4-1のCMakeLists.txtで Visual Studio 用に生成することが出来ないわけではありません。
普通にコマンド・プロンプトを起動して、hello.cppとCMakeLists.txtのあるフォルダへ移動します。
そして下記コマンドにより、ソリューションを生成します。
> mkdir build > cd build > cmake .. (生成状況略)
Visual Studioがインストールされていれば-Gを省略することができますので、省略しています。
なお、このコマンド・プロンプトはこの直ぐ後で使うので立ち上げたままにしておいて下さい。
上記操作によりbuildフォルダにhello.slnが出来ているのでダブルクリックしてVisual Studioを起動して下さい。そして、ソリューション エクスプローラでhelloを右クリックして「スタートアップ プロジェクトに設定(A)」を選択し、CTRL+F5(実行後コマンド・プロンプトが一時停止します。)にてビルドと実行を行って下さい。
さて、結果なのですが、デフォルトでは「アクティブソリューション構成(C)」は Debug ですのでデバッグ・ビルドされることを期待しますね。なので、BUILD_TYPEには”Debug”が設定されていることをつい期待してしまいます。
Hello, World!! BUILD_TYPE=Release
この辺の振る舞いに私は本当に振り回されパニックでした。
4-3.種明かし
しかし、上記の振る舞いは CMake としては当たり前なのです。
CMAKE_BUILD_TYPEを if 文で判定して分岐し、CMAKE_CXX_FLAGSへの設定値を切り替えていますが、この if 文は CMake コマンドでビルド・システム(≒*.sln
)を生成する時に解釈されるものですから、生成されてしまった後の切り替えに対応していません。
今回は、CMAKE_BUILD_TYPEに”Debug”が設定されていたら BUILD_TYPEシンボルに”Debug”を設定し、そうでなければ”Release”を設定しています。CMAKE_BUILD_TYPEを指定しませんでしたので、BUILD_TYPEシンボルには”Release”が設定されているのです。
試しに、CMAKE_BUILD_TYPEに Debug と指定してみましょう。
> cmake .. -DCMAKE_BUILD_TYPE=Debug (生成状況略)
Visual Studioに戻ると「外部で変更されたので、再度読み込むか?」のようなダイアログが表示されるので「全てに適用(A)」を押して読み込んで下さい。
そして、Ctrl+F5でビルドして実行すると、めでたくDebugと表示されます。
Hello, World!! BUILD_TYPE=Debug
4-4.だがしかし!
「アクティブソリューション構成(C)」をReleaseに切り替えて、Ctrl+F5で実行してみて下さい。
鋭い方の中には既に予想されている方もいると思いますが、結果はDebugと表示されます。
上述したように、CMAKE_BUILD_TYPEの内容で切り替える処理は、ソリューション(*.sln)
を生成する際に解釈されます。
つまり、「アクティブソリューション構成(C)」を切り替えても変わらないのです。
これでは事実上役に立ちません。デバッグ・ビルドとリリース・ビルドでコンパイル・オプションを切り替えたいのに、「アクティブソリューション構成(C)」を切り替える度に いちいちCMake 生成をやり直す必要があるのでは困ります。ミスを頻発します。リリース・ビルドしたつもりで、CMakeでDebug生成したものを配布してしまったら目も当てられません。
5.CMakeはこの状況に対処しています!
シングル・コンフィグレーションのビルド・ツールは生成時にDebug/Releaseを切り替え、マルチ・コンフィグレーションのビルド・ツールはビルド時にDebug/Releaseを切り替えます。前者は素直ですが、後者はCMakeが生成した後、もうCMakeが走っていない時に対処する必要があるので、技術的にも難しそうに感じます。
さて、最終的に生成するものは、MSBuildの場合、*.vcxproj
ファイルです。この中にDebugビルド用とReleaseビルド用の両方のビルド設定が入っています。
しかし、CMakeのif文での分岐は、CMAKE_BUILD_TYPEの条件と複合して他の条件を入れることもできますし、それらをDebugとRelaseに分けて整理し、*.vcxproj
ファイルを生成するのは至難の業と思います。
5-1.CMakeの解
CMakeはこの状況に対し明快な解を採用しています。例えば、先程のコンパイル・オプションを指定する変数CMAKE_CXX_FLAGSにDebug用とRelease用の変数を設けるのです。
変数名 | 使われ方 |
---|---|
CMAKE_CXX_FLAGS | DebugとRelease両方(*2) に共通のオプションを定義する |
CMAKE_CXX_FLAGS_DEBUG | Debugビルド時に追加するオプションを定義する |
CMAKE_CXX_FLAGS_RELEASE | Releaseビルド時に追加するオプションを定義する |
これなら、CMake自体の構造も比較的単純さを保てます。
そして、これらはシングル・コンフィグレーションとマルチ・コンフィグレーションの両方で機能しますので、if文などで CMAKE_BUILD_TYPE の内容を判定するのではなく、CMAKE_CXX_FLAGS_DEBUGやCMAKE_CXX_FLAGS_RELEASEなどを設定するだけでシングルとマルチの両方に対応した CMakeLists.txt を書くことができるのです。頭のいい対処と思います。
DebugとRelease両方(*2)
解り易さのため、DebugとRelease両方と書きましたが、実際にはRelWithDebInfo、MinSizeRelやユーザ定義のビルド・モードも含む全てのビルド・モード共通のコンパイル・オプションを設定します。
この仕組は、変数名だけでなく、プロパティ名、generator-expressionsで採用されています。公式のマニュアルへリンクを貼っています。変数とプロパティでは、<CONFIG>
と書かれている部分に DEBUG や RELASE 等のビルド・モード名を大文字にしたものが入ります。generator-expressionsは$<CONFIG>
にてビルド・モードを調べることができます。
変数とプロパティは当講座の比較的早いタイミングで解説します。generator-expressionsは頻繁には使わないし難しいので解説する場合でもかなり後の方になりそうです。私自身十分に理解しているとは言い難いので解説を見送るかも知れません。
5-2.サンプルと実験
4章の冒頭のhello.cppをシングルとマルチ・コンフィグレーションの両方に対応した CMakeLists.txt を用意しました。
cmake_minimum_required(VERSION 3.5.1) project(hello) if(MSVC) string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /DBUILD_TYPE=\\\"Debug\\\"") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /DBUILD_TYPE=\\\"Release\\\"") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DBUILD_TYPE=\\\"Debug\\\"") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DBUILD_TYPE=\\\"Release\\\"") endif() add_executable(hello hello.cpp)
4-1.のCMakeLists.txtと見比べるとスッキリし、かつ、マルチ・コンフィグレーションにも対応するのでスグレモノです。(って、Hello, World! なので大げさすぎですね。)
これを使って、シングル・コンフィグレーションはgcc、MinGW、NMake、マルチ・コンフィグレーションはVisual Studioで生成し実行してみました。(gccはubuntu 16.4、その他は全てWindows 10です。)
処理系 | 実行 | CMAKE_BUILD_TYPE | ||
指定しない | “Debug” | “Release” | ||
gcc 5.4.0 | ./hello | BUILD_TYPE=<None> | BUILD_TYPE=Debug | BUILD_TYPE=Release |
MinGW 8.1.0 | hello.exe | BUILD_TYPE=<None> | BUILD_TYPE=Debug | BUILD_TYPE=Release |
NMake | hello.exe | BUILD_TYPE=Debug | BUILD_TYPE=Debug | BUILD_TYPE=Release |
Visual C++ | Debug\hello.exe | BUILD_TYPE=Debug | BUILD_TYPE=Debug | BUILD_TYPE=Debug |
Release\hello.exe | BUILD_TYPE=Release | BUILD_TYPE=Release | BUILD_TYPE=Release |
シングル・コンフィグレーションではCMAKE_BUILD_TYPE指定通りにコンパイラ・オプションが反映されています。
マルチ・コンフィグレーションでもCMAKE_BUILD_TYPEを書くことはできますが、丸っと無視され「アクティブソリューション構成(C)」の設定が反映されています。
5-3.ちょっと気になる結果について
シングル・コンフィグレーションで、CMAKE_BUILD_TYPEを指定しなかった時の振る舞いが謎です。
CMakeのジェネレータは処理系毎に別途実装されているようですので、gccとNMakeでは振る舞いが異なるのだろうと思います。
念のため、どんなふうに振る舞っているのか確認してみました。
Makefileにはコンパイラ・オプション変数CXX_FLAGSがあります。grepしてみたところ、CMakeはこれをCMakeFiles\hello.dir\flags.make
で定義しているようです。その内容を調べました。
処理系 | CMAKE_BUILD_TYPE | ||
指定しない | “Debug” | “Release” | |
gcc 5.4.0 | -Wall -std=c++11 | -Wall -std=c++11 -g -DBUILD_TYPE=\”Debug\” | -Wall -std=c++11 -O3 -DNDEBUG -DBUILD_TYPE=\”Release\” |
MinGW 8.1.0 | -Wall -std=c++11 | -Wall -std=c++11 -g -DBUILD_TYPE=\”Debug\” | -Wall -std=c++11 -O3 -DNDEBUG -DBUILD_TYPE=\”BUILD_TYPE=Release\” |
NMake | /DWIN32 /D_WINDOWS /GR /EHsc /W4 /EHsc /MDd /Zi /Ob0 /Od /RTC1 /DBUILD_TYPE=\”Debug\” | /DWIN32 /D_WINDOWS /GR /EHsc /W4 /EHsc /MDd /Zi /Ob0 /Od /RTC1 /DBUILD_TYPE=\”Debug\” | /DWIN32 /D_WINDOWS /GR /EHsc /W4 /EHsc /MD /O2 /Ob2 /DNDEBUG /DBUILD_TYPE=\”Release\” |
gccとMinGWは、CMAKE_CXX_FLAGS_DEBUG、CMAKE_CXX_FLAGS_RELEASEについてCMAKE_BUILD_TYPEで指定したものが追加映され、指定していないものは追加されないようです。
NMakeはCMAKE_BUILD_TYPE指定なしならDebugと指定したものとして処理しているようですね。何か処理系の都合があるのかも知れません。
6.まとめ
今回、シングル・コンフィグレーションとマルチ・コンフィグレーションについて解説してみました。
意外に混乱を招く部分で、これらの振る舞いや設定方法も理解していないと、マルチ・プラットフォーム対応が困難であることを説明できたと思います。
基本的には、CMakeLists.txt内でCMAKE_BUILD_TYPE変数を使わないことと思います。これを使ってしまうとVisual StudioやXcodeでビルド時にビルド・モードを切り替えるとハマる可能性が飛躍的に高くなります。
ところで、DebugとReleaseはビルド時に切り替えるのに何故に「構成」と呼ぶのか不思議でした。どうも私が Visual Studio に慣れていたからということのようです。昔は主にアセンブラを使っていました。少なくとも当時のアセンブラに最適化はなくDebugビルド、Releaseビルドなんてありません。リンク後にCPUへ書き込む際に不要なデバッグ情報を削ぎ落とすという使い方でした。しかし、linuxやunixのツールは、まずは ./Configure してからビルドすることがほとんどです。この時点で「構成」するということのようです。
さて、次回は、予定変更して先延ばしした 良く使うCMakeコマンドについて解説したいと思います。お楽しみに!