本文を読み飛ばす

CMakeでCRTライブラリを静的リンクするVisual Studioプロジェクトを出力する

CRT ライブラリを静的リンクする (/MT コンパイルオプションを使う)よう設定した Visual Studio のプロジェクトファイル (.vcxproj) を CMake で生成する方法について勉強したので、備忘録。

なお、本質的な内容は 公式 Wiki の FAQ に掲載されている方法 と変わらない。 ただ Wiki の記載をすんなりと理解できなかったので、自分なりにかみ砕いて理解して、 自分の価値観的なメリット・デメリットを沿えつつ書き起こしてみた次第。

使用環境・ツール

  • CMake 3.8.0
  • Visual Studio 2015 Update 3
  • Windows 10 Pro

実現すること

Visual Studio のプロジェクト設定画面

Visual Studio 2015 的に書くと、C/C++プロジェクトのプロパティのうち 「構成プロパティ」→「C/C++」→「ランタイム ライブラリ」欄が、 標準の「マルチスレッド DLL (/MD)」ではなく「マルチスレッド (/MT)」に設定されているような .vcxproj を出力することを目的とする。 もちろん、デバッグビルド版でも同様に CRT を静的リンクさせるようにする。

なお、以下で記す「ビルド種別」という言葉は CMake の Build Type を指す。つまり「Debug ビルドか Release ビルドか MinSizeRel ビルドか RelWithDebInfo ビルドか」 を表している。

実現方法

基本的な考え方は、「コンパイラが Visual Studio ならばコンパイラオプションに /MD ではなく /MT を使わせる」よう記述する、というもの。 コマンドラインオプション文字列を直接書いてしまうあたりアドホックな印象を受けるかもしれないけれど、 CMake 自体が if (MSVC) などというコンパイラ種別での条件分岐を書くのが当然な世界なので、 こういうものと割り切って欲しいところ。

各ビルド種別用のコマンドラインオプションを動的に書き換える方法

考え方として一番単純なのは、トップレベルの CMakeLists.txt で project() コマンドなどの呼び出しによりジェネレータが (したがって標準のコンパイルオプション一式も)確定した後に、 CMake が自動生成した各ビルド設定用コマンドラインオプションを書き換えてしまう方法だと思う。

まず、トップレベルの CMakeLists.txt にて project() コマンドの呼び出し直後に、 次のようなコードを挿入する:

if(MSVC)
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_DEBUG            ${CMAKE_C_FLAGS_DEBUG})
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_MINSIZEREL       ${CMAKE_C_FLAGS_MINSIZEREL})
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE          ${CMAKE_C_FLAGS_RELEASE})
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELWITHDEBINFO   ${CMAKE_C_FLAGS_RELWITHDEBINFO})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_DEBUG          ${CMAKE_CXX_FLAGS_DEBUG})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_MINSIZEREL     ${CMAKE_CXX_FLAGS_MINSIZEREL})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE        ${CMAKE_CXX_FLAGS_RELEASE})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELWITHDEBINFO ${CMAKE_CXX_FLAGS_RELWITHDEBINFO})
endif(MSVC)

コードの意味は「コンパイラが Visual Studio ならば CMAKE_C_FLAGS_DEBUG など 8 変数の値に含まれる /MD という文字列を /MT に置換せよ」といったところ (単純な文字列置換なので /MDd/MTd に置換される)。 CMake の細かい仕様は知らないけれど、どうやら Visual Studio の C/C++ コンパイラ (cl.exe) 向けのコンパイルオプション文字列を設定しておけば、そのコンパイルオプションが使われるような .vcxproj が出力されるらしい。したがって、上記のようにビルド設定ごとに C 用と C++ 用のコンパイルオプションを書き換えてしまえば目的を達成できる。

この方法を使うと、トップレベルの CMakeLists.txt は次のような感じになる。

cmake_minimum_required(VERSION 3.0)

project(hoge_service
        VERSION 2017.5.27
        LANGUAGES C CXX)

# Visual Studioでのコンパイル時は、CRTライブラリを静的リンクさせる
if (MSVC)
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_DEBUG            ${CMAKE_C_FLAGS_DEBUG})
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_MINSIZEREL       ${CMAKE_C_FLAGS_MINSIZEREL})
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE          ${CMAKE_C_FLAGS_RELEASE})
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELWITHDEBINFO   ${CMAKE_C_FLAGS_RELWITHDEBINFO})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_DEBUG          ${CMAKE_CXX_FLAGS_DEBUG})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_MINSIZEREL     ${CMAKE_CXX_FLAGS_MINSIZEREL})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE        ${CMAKE_CXX_FLAGS_RELEASE})
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELWITHDEBINFO ${CMAKE_CXX_FLAGS_RELWITHDEBINFO})
endif ()

add_subdirectory(hoge_client)
add_subdirectory(hoge_server)
add_subdirectory(hoge_test)

この方法のメリットは、やっていることが単純なので CMake に詳しくなくとも何となく理解しやすいこと、 および CMakeLists.txt 以外のファイルを導入しないで済むことだと思う。

デメリットは、CMake が走るたびにコマンドラインオプションを強制的に置換してしまうので cmake コマンドの -D オプションccmake および cmake-gui で個々の作業者ローカル環境でのコンパイルオプション変更ができなくなること。 もっとも、上記の例では string コマンドCRT ライブラリのリンク方法だけを置換しているので現実的に問題になることは無いと思う (そこを一時的にでも変えたがる人や状況は考えにくい)。 もし string コマンドで部分的に置換する代わりに set コマンドで 全体を丸ごと置換するようにした場合、もしかしたら問題になるかもしれない。

各ビルド種別用のコマンドラインオプションの初期値を書き換える方法

ところで先の例では CMake が生成したコンパイルオプションを書き換える方法を採用していたけれど、 そもそも CMake は Visual Studio 用のコンパイルオプションの初期値を内部的に持っているからこそ CMAKE_C_FLAGS_DEBUG などの変数値を生成できるわけだ。ならばその初期値を書き換える形でも 目的達成できるはず…ということで、その方法を続けて記す。 なお以下の説明では C 言語用の Debug ビルド用コンパイルオプション CMAKE_C_FLAGS_DEBUG についてだけ説明するけれど、 他も同様ということで適宜説明を心の中で補って欲しい。

まず、project() コマンドなどを呼び出すとコマンドラインオプションが確定する、つまり初期値が CMAKE_C_FLAGS_DEBUG などにコピーされるので、その前に置換処理を仕込む必要がある。 この初期値は CMAKE_C_FLAGS_DEBUG_INIT という変数で表されるので、 project() コマンド呼び出し前にこの変数を都合良く書き換えてしまえば目的達成できる。 しかし、厄介なことに project() コマンドを実行する前の段階ではコンパイラが確定していないため if (MSVC) と書いても if の中に入ることは絶対に無く、先の例と同じような置換命令を書いても うまく動作してくれない。まあ、コンパイラが確定する前にコンパイラ依存の処理を書こうというのだから 普通に書けないのは仕方が無い。そこで、CMake が提供する特殊な機構を使う。具体的には、 CMAKE_USER_MAKE_RULES_OVERRIDE という変数 に「コンパイルオプションの初期値を書き換える内容を記したファイルの名前」を設定する。 なおファイル名は一つしか指定できない点に注意。

まず、たとえば c_flag_overrides.cmake というファイルを以下の内容で用意して:

# Visual Studioでのコンパイル時は、CRTライブラリを静的リンクさせる
if (MSVC)
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_DEBUG_INIT            "${CMAKE_C_FLAGS_DEBUG_INIT}")
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_MINSIZEREL_INIT       "${CMAKE_C_FLAGS_MINSIZEREL_INIT}")
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE_INIT          "${CMAKE_C_FLAGS_RELEASE}_INIT")
    string(REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELWITHDEBINFO_INIT   "${CMAKE_C_FLAGS_RELWITHDEBINFO_INIT}")
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_DEBUG_INIT          "${CMAKE_CXX_FLAGS_DEBUG_INIT}")
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_MINSIZEREL_INIT     "${CMAKE_CXX_FLAGS_MINSIZEREL_INIT}")
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE_INIT        "${CMAKE_CXX_FLAGS_RELEASE_INIT}")
    string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT}")
endif (MSVC)

これのファイル名を CMAKE_USER_MAKE_RULES_OVERRIDE 変数に設定する:

cmake_minimum_required(VERSION 3.0)

# 標準のC/C++用コンパイルオプションをカスタマイズする
set(CMAKE_USER_MAKE_RULES_OVERRIDE
    /path/to/c_flag_overrides.cmake)

project(hoge_service
        VERSION 2017.5.24
        LANGUAGES C CXX)

add_subdirectory(hoge_client)
add_subdirectory(hoge_server)
add_subdirectory(hoge_test)

この方法のメリットは、強いて言うなら、トップレベル CMakeLists.txt のコンパイラ依存命令が (見た目の上では)少なくなることくらいだろうか。

デメリットは、管理対象ファイルが増えること、トップレベル CMakeLists.txt を見るだけでは 何が起こるのか分かりにくいこと、そして前の実現方法と同様に cmake-D オプションや ccmakecmake-gui でコンパイルオプションのカスタマイズができない点が挙げられると思う。

もし CMAKE_USER_MAKE_RULES_OVERRIDE に複数のファイルを指定できるなら、 いろいろな観点でのコンパイルオプション変更用 .cmake ファイルを用意することで 可読性・管理性を改善できると思う。しかし一つしか指定できないのでファイル名も 「オプションをオーバーライドする」といった役に立たない名前にせざるをえず、 正直イマイチだなと思う。

後書き

個人的には、初期値を書き換える方が良い理由が見つからなかった。 コンパイラ確定後にコンパイラオプションを動的に書き換える方法で、良いように思う。