はじめに
CMakeを使い始めて半年以上経ちますが、まだまだ把握していない機能が多いです。
勉強のためYouTubeに上がっているCMakeに関する動画をいくつか見てきて、非推奨なコマンドや書き方があることがわかったので、それらをここにまとめておきます。
参考資料
- CppCon 2017: Mathieu Ropert “Using Modern CMake Patterns to Enforce a Good Modular Design”
- C++Now 2017: Daniel Pfeifer “Effective CMake"
- Embracing Modern CMake - How to recognize and use modern CMake interfaces by Stephen Kelly
- CMake Best Practice by Fujii Hironori
- CMAKE - Introduction and best practices by Daniel Pfeifer
- Effective Modern CMake
ガイドライン
基本
現代的なCMakeの使い方は次の3点で表されます。
- ターゲット(target)を基本としたビルドシステム
- 依存関係を一箇所で指定する
- ターゲットがそれに依存するものへコンパイルおよびリンクに関する情報を提供する
すなわち、 target_
で始まるコマンドを使用し、ターゲットに対する要件を指定する ことが基本となります。
CMakeのバージョンは2.8.12〜、実務上は3.0.0以降を指します。
現在は非推奨となっているコマンド
下記コマンドはターゲットに関わらず設定してしまうため使うべきではありません。
include_directories
-
add_definitions
,add_compile_definitions
,add_compile_options
link_directories
link_libraries
例えば、include_directories
はコンパイラのファイルを探すパスに与えられたディレクトリを追加します。このコマンドを使用すると、定義したライン以降の全てのターゲットが指定したディレクトリをインクルードしてしまいます。include_directories
の代わりにtarget_include_directories
を使うようにしましょう。
include_directories(${MYLIB_INCLUDE_DIRS})
add_library(mylib SHARED mylib.cpp)
#このライン以降もFOO_INCLUDE_DIRSは有効
add_library(mylib SHARED mylib.cpp)
target_include_directories(mylib
PUBLIC ${MYLIB_INCLUDE_DIRS}
)
ただし、他のターゲットに属するディレクトリに対しては、target_include_directories
を用いるべきではありません。すなわち
add_library(mylib SHARED ${MYLIB_SRC_FILES})
add_executable(myapp myapp.cpp)
target_link_libraries(myapp PRIVATE mylib)
target_include_directories(myapp PRIVATE ${MYLIB_INCLUDE_DIRS})
add_library(mylib SHARED ${MYLIB_SRC_FILES})
target_include_directories(mylib PUBLIC ${MYLIB_INCLUDE_DIRS})
add_executable(myapp myapp.cpp)
target_link_libraries(myapp PRIVATE mylib)
# MYLIB_INCLUDE_DIRSをmylibのインクルードディレクトリとしてPUBLICスコープで指定すれば、
# 自動的にmylibに依存するmyappのインクルードディレクトリとなる。
CMAKE_<LANG>_FLAGS
にオプションを足さない
CMAKE_<LANG>_FLAGS
の代わりにtarget_compile_options
を使いましょう。
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
target_compile_options(mylib PUBLIC -Wall)
CMAKE_CXX_FLAGS
やtarget_compile_options
に-std=c++11/c++14/c++17/c++20
を加えない
CMAKE_CXX_STANDARD
(CMake 3.1以降)を使うか、target_compile_features
にcxx_std_11/cxx_std_14/cxx_std_17/cxx_std_20
を加えましょう(CMake 3.8以降)。
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
target_compile_features(mylib PUBLIC cxx_std_17)
変数を使い過ぎない
set
を使用して定義される変数には以下のような問題点があります。
- つづりを間違えやすい
- 他の文脈へリークしやすい(定義した以降のあらゆる場所で使用可能)
- 変数が依存するもののスコープが不明
- 変数の内容やその正しさがチェックされない
変数を使う必要がなくてもかける場合は使わないようにしましょう。
特にファイルをターゲットに追加する場合、変数の代わりにtarget_sources
を使いましょう。
set(MYLIB_SRCS
file1.cpp
file2.cpp
)
add_library(mylib ${MYLIB_SRCS})
add_library(mylib
PRIVATE
file1.cpp
file2.cpp
)
PRIVATE
/PUBLIC
/INTERFACE
を適切に使う
これらのキーワードは、コマンドのターゲットおよびそのターゲットに依存するターゲットに対する必要性を表します。
キーワード | ターゲットが必要とする | ターゲットに依存するターゲットが必要とする |
---|---|---|
PRIVATE | ||
PUBLIC | ||
INTERFACE |
ヘッダーのみのライブラリにはINTERFACE
を使います。
if文よりもGenerator Expressionを使う
CMakeは
- Configure - CMakeLists.txtをパースしキャッシュ変数をCMakeCache.txtに書き込む
- Compute - 依存関係を計算する
- Generate - ビルドファイル(Makefile等)を作成する
の三段階に分けて実行されます。
この中で、CMakeのif文(if()
/elseif()
/else()
/endif()
)はConfigure時に実行されるのに対し、Generator Expression($<...>
)はGenerateの直前に評価されるという違いがあります。この評価時期の違いによりCMakeが想定と異なる動作を引き起こすことがあります。
したがって、Configureをした後に評価するもの:
-
CONFIG
(Release/Debug/...) TARGET_PROPERTY
TARGET_POLICY
COMPILE_FEATURES
LOCATION
に対してはGenerator Expressionsを使うようにします。
例えばVisual C++の場合、CMakeLists.txtをConfigureするのはCMakeLists.txtを含むフォルダを開いた時のみであり、ビルドタイプ(Release/Debug等)はデフォルトでDebug
として評価されます:
When you choose File | Open | Folder to open a folder containing a CMakeLists.txt file, the following things happen:
(中略)
- Visual Studio runs CMake.exe and generates the CMake cache for the default configuration, which is x86 Debug. ...
したがって、その後にIDE上でビルドタイプをRelease
に変更しても既にキャッシュ変数として保存されたCMAKE_BUILD_TYPE
は変わらないため、CMakeスクリプト内でCMAKE_BUILD_TYPE
を使って評価する部分はDebug
のまま評価されてしまうということが起きます。if文でなくGenerator Expressionを使い、CMAKE_BUILD_TYPE
を使わないようにすると、この問題を避けることができます。
ビルドタイプ(Release/Debug)に応じてファイルを変更したい場合、以下の2つは同じことを意図していますが、if文を使っている方はVisual C++で使うとビルドタイプを変えた時に適切に反映されません。
set(MYAPP_SRCS
main.cpp
)
if (CMAKE_BUILD_TYPE STREQUAL DEBUG)
list(APPEND MYAPP_SRCS logger_debug.cpp)
else()
list(APPEND MYAPP_SRCS logger_release.cpp)
endif()
add_executable(myapp ${MYAPP_SRCS})
add_executable(myapp
main.cpp
$<IF:$<CONFIG:Debug>,logger_debug.cpp,logger_release.cpp>
)
file(GLOB)
を使わない
file(GLOB)
は、CMakeを実行するたびに条件に合致するファイルのリストを自動的に作成する非常に便利なコマンドです。ところがこのコマンドはIDEで使う場合うまく動作しない場合があります(Visual C++等)。したがって、常にcmakeをコマンドラインから実行するような場合でない限り使うべきではありません。
ディレクトリごとにCMakeLists.txtを作成し、add_subdirectory
とtarget_sources
を使って再帰的にファイルを明示して追加しましょう。
# /src/CMakeLists.txt
# 子ディレクトリ内のソースファイルを再帰的に探索し変数へ追加
file(GLOB_RECURSE MYLIB_SRCS "*.cpp")
add_library(mylib ${MYLIB_SRCS})
# /src/CMakeLists.txt
add_library(mylib)
add_subdirectory(dir1)
add_subdirectory(dir2)
# ... 子ディレクトリを同様に追加していく
# /src/dir1/CMakeLists.txt
# CMake-3.12まで
target_sources(mylib
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/file1.cpp
${CMAKE_CURRENT_SOURCE_DIR}/file2.cpp
# 絶対パスで指定しないとエラーになるので注意
)
# CMake-3.13以降
cmake_policy(SET CMP0076 NEW) # CMakeが自動的に相対パスを絶対パスへ変換する
target_sources(mylib
PRIVATE
file1.cpp
file2.cpp
)
マクロの代わりにfunction
を使う
マクロは呼び出す側のスコープにある変数を上書きしてしまいます。自身のスコープを持つfunction
を使いましょう。親ディレクトリのスコープにある変数を書き換える場合はset(<variable> <value>... PARENT_SCOPE)
を使いましょう。
キャッシュ変数には必ず接頭辞(prefix)を付ける
キャッシュ変数はグローバル変数です。名前の衝突を避けるために接頭辞をつけましょう。
CMAKE_SOURCE_DIR
を使わない
代わりにCMAKE_CURRENT_SOURCE_DIR
、PROJECT_SOURCE_DIR
、<PROJECT-NAME>_SOURCE_DIR
を使いましょう。
CMAKE_SOURCE_DIR
はトップレベルのディレクトリを指します。異なるプロジェクトがネストしている場合、自身のプロジェクトのルートディレクトリ以外のパスを示すので使わないようにしましょう。
必要ない限りライブラリの種類を指定しない
ビルドするユーザーが静的・共有ライブラリを決められるようにしましょう。
グローバルオプションBUILD_SHARED_LIBS
を使って選択することもできますが、各ライブラリごとにオプションを設定しておく方がいいです。
option(MYLIB_BUILD_SHARED_LIBS "build mylib as a shared library" ON)
if (BUILD_SHARED_LIBS AND MYLIB_BUILD_SHARED_LIBS)
add_library(mylib SHARED <files>...)
else()
add_library(mylib STATIC <files>...)
endif()