CMake — Cross-platform Make
環境に合わせた Makefile を自動生成する。
似たようなことをする configure スクリプトと比べて動作が高速で、
ライブラリの依存関係なども簡潔・柔軟に記述できる。
configure ではそれを生成する開発者だけが
autotools を使うのに対して、
CMakeでは開発者と利用者の双方がCMakeをインストールして使う。
https://cmake.org/cmake/help/latest/
基本
CMakeLists.txt を各ディレクトリに配置して、階層的に管理する。
プロジェクトのトップに置くものは、以下のようなコマンドで始める必要がある。
cmake_minimum_required(VERSION 3.15)
project(helloworld
VERSION 0.1.0
LANGUAGES CXX)
add_executable() や add_library() でビルドターゲットを作成し、
target_*() でそれらの設定を整えて、
install() でインストールする対象や行き先を指定する、というのが基本の流れ。
add_executable(a.out hello.cpp)
target_compile_options(a.out PRIVATE -Wall -Wextra -pedantic)
install(TARGETS a.out)
Commands
https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html
Scripting commands
configure_file(<input> <output> [COPYONLY] [@ONLY]): ファイルの一部を置換しつつ複製する。 例えば@PROJECT_VERSION@などを含むconfig.hpp.inに値を埋め込んでconfig.hppを生成するとか。execute_process(COMMAND ...): シェル無しで外部コマンドを直接実行する。COMMANDを複数書くだけでパイプラインも組めるし、標準出力も取れる。 親プロジェクトから呼ばれることも考えてWORKING_DIRECTORYは明示的に指定すべし。function(<name> [args...])foreach(var IN LISTS list)message(STATUS "Hello world!")option(VARIABLE "Message" ON)set(VARIABLE value)PARENT_SCOPEで親スコープの変数に代入できるが、現在のスコープではそれを参照できない。cmake_path()パス操作全般。 3.20 より古い環境ではget_filename_component()。
Project commands
https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html#project-commands
外部のCMakeプロジェクトを利用する:
find_package(<name> [version] [REQUIRED] ...): ビルド・インストール済みのを取り込む。add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]): インストール前のソースツリーを取り込む。
ターゲットを定義する:
add_executable(<name> [EXCLUDE_FROM_ALL] ...)add_library(<name> [STATIC|SHARED|OBJECT] [EXCLUDE_FROM_ALL|IMPORTED] ...)option Mac Linux Win description STATIC.a.a.libオブジェクトをまとめて目次をつけたarchive。静的リンクで複製は生じるが最適化される。 SHARED.dylib.so.dllオブジェクトをまとめて実行可能ファイル一歩手前まで加工したもの。動的リンクでストレージにもメモリにも1つだけあればいいので効率はいいけどRPATHのお世話が必要。 OBJECT.o.o.objコンパイル単位のオブジェクト。ライブラリを経由しないぶん STATICより効率的かと思いきや、リンク時に最適化されず、使わないコードまで保持される。使い道はライブラリを複数作りたい時くらい?
ターゲットのプロパティを追加する:
set_target_properties(<target> PROPERTIES p1 v1 p2 v2 ...)target_compile_definitions(<target> <INTERFACE|PRIVATE|PUBLIC> ...)target_compile_features(<target> <P|P|I> ...)target_compile_options(<target> [BEFORE] <I|P|P> ...)target_sources(<target> <I|P|P> ...)target_include_directories(<target> [SYSTEM] [BEFORE] <I|P|P> ...): ソースツリー内とインストール先でヘッダーファイルの置き方が変わる場合などに使う。 外部からインポートしたパッケージのヘッダーにはこちらを使わず、次の関数を使う。target_link_libraries(<target> <I|P|P> ...): この関数でターゲット間の依存関係を繋げていくのがCMakeの肝。 共有ライブラリのオプション-L -lだけではなく、ヘッダーのオプション-Iもお世話してくれる。- ターゲットなしの
include_directories()link_directories()link_libraries()などはディレクトリ単位で影響が及ぶ亜種で、非推奨。
install(TARGETS)install(<FILES|PROGRAMS>)install(DIRECTORY)install(EXPORT): 外部のCMakeから使いやすくするためのconfigをインストールする。 似て非なるexport()はビルドツリーにあるものを使わせるための謎コマンド。
オプション
PRIVATE- このターゲットをビルドするときだけ使い、これを利用するときには参照させない。 例えば「このプロジェクトのライブラリをビルドするにはBoostヘッダーが必要だけど、 これを利用するときにそれらのパスを知る必要はない」とか。
INTERFACE- このターゲットでは使わないけど、これを利用するときには参照させる。 例えば、ヘッダーライブラリを作る場合とか。
PUBLIC- このターゲットにもこれを利用するターゲットにも使う。使う場面あるかな?
EXCLUDE_FROM_ALLmake [all]から外れて、明示的なターゲット指定でのみビルドされるようになる。
Variables
https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html
Variables that Provide Information
PROJECT_SOURCE_DIR,PROJECT_BINARY_DIR: 直近のproject()におけるソースツリー、ビルドツリーの最上階。CMAKE_SOURCE_DIR,CMAKE_BINARY_DIR: ソースツリー、ビルドツリーの最上階。 他のプロジェクトからadd_subdirectory()で使われる場合、PROJECT_*よりも上になる。CMAKE_CURRENT_SOURCE_DIR,CMAKE_CURRENT_BUILD_DIR: ソースツリー、ビルドツリーにおける現在地。PROJECT_NAME,PROJECT_VERSION:project()で設定したやつ。CMAKE_SKIP_RPATHCMAKE_VERBOSE_MAKEFILE: とりあえずON
Variables that Change Behavior
BUILD_SHARED_LIBS: STATIC/SHAREDが明示されていないadd_library()でどっちをビルドするか。CMAKE_BUILD_TYPE: Debug, Release, RelWithDebInfo, MinSizeRel.CMAKE_INSTALL_PREFIX: configureでの--prefixに相当CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULTCMAKE_PREFIX_PATH: パッケージやファイル探索find_*()の候補パスを追加する
Variables that Describe the System
APPLE, UNIX, WIN32
Variables that Control the Build
CMAKE_BUILD_RPATHCMAKE_BUILD_WITH_INSTALL_NAME_DIRCMAKE_BUILD_WITH_INSTALL_RPATHCMAKE_INSTALL_NAME_DIRCMAKE_INSTALL_RPATHCMAKE_INSTALL_RPATH_USE_LINK_PATHCMAKE_MACOSX_RPATH
Environment variables
https://cmake.org/cmake/help/latest/manual/cmake-env-variables.7.html
変数として自動的に利用可能になったりはせず、
$ENV{VAR}
みたいな形で参照するのが基本。
ただし、一部の環境変数はCMakeの変数の初期値として採用される。
e.g., CMAKE_PREFIX_PATH, CXX, <PackageName>_ROOT, etc.
何が渡っているかは cmake -E environment で確認できる。
CMAKE_EXPORT_COMPILE_COMMANDS- 定義しておくとコンフィグ時に
compile_commands.jsonを生成してもらえる。 これで clangd にコンパイルオプションを伝えられる。 ソースファイルの親ディレクトリを辿るだけでなくbuildという名のサブディレクトリも探してくれるので-B buildの慣習に従っていればコピーやシムリンクも不要。
C++
add_compile_options(
-Wall -Wextra -pedantic
$<IF:$<BOOL:$<CONFIG>>,,-O2>
$<IF:$<BOOL:$<CONFIG>>,,-g>
$<$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},x86_64>:-march=native>
$<$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},arm64>:-march=armv8.4-a>
)
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF)
CMAKE_CXX_* のようなグローバル設定を使わず
target_*() でターゲットごとに設定するのが今後の主流。
CMAKE_CXX_KNOWN_FEATURES
に cxx_std_17 などの便利なメタタグが導入されたのは CMake 3.8 から。
グローバルなビルドオプションは利用者側が
-DCMAKE_BUILD_TYPE=Release のようにタイトルケースで指定するのが慣例。
ヘッダーが別のヘッダーを読み込む transitive include によって、
コンパイル時の負荷が増大したり #include し忘れが隠蔽されたりすることがある。
この影響をなるべく小さくするための変更がlibc++に入った。
後方互換性のために今のところデフォルト無効だが、次のような定義で有効にできる:
add_compile_definitions("_LIBCPP_REMOVE_TRANSITIVE_INCLUDES")
| Predefined variable | default |
|---|---|
CMAKE_CXX_FLAGS |
|
CMAKE_CXX_FLAGS_DEBUG |
-g |
CMAKE_CXX_FLAGS_MINSIZEREL |
-Os -DNDEBUG |
CMAKE_CXX_FLAGS_RELEASE |
-O3 -DNDEBUG |
CMAKE_CXX_FLAGS_RELWITHDEBINFO |
-O2 -g -DNDEBUG |
#ifndef NDEBUG なコードを残しつつ、
そこそこ速くコンパイル&実行したい、
という組み合わせ -O2 -g は用意されていないので自分で定義する。
例えば上記の例のように無指定のときのデフォルトとするか、
set(CMAKE_CXX_FLAGS_DEV "-O2 -g") として明示的に
-DCMAKE_BUILD_TYPE=Dev で利用するとか。
CMAKE_BUILD_TYPE を普通の変数として参照するのは避けたほうがいい。
うっかり case-sensitive な処理を書いてしまうリスクもあるし、
multi-config generator ではコンフィグ時に未定義。
$<CONFIG>
などの generator expression を使えばそれらを回避できる。
Generator expressions
https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html
ビルド時の状態に応じてに変数を評価する仕組み。
コンフィグ時に評価される if() とは使い方が異なる。
例えば、プロジェクト内のビルド時と、外部パッケージとして利用される時とで、 インクルードパスを使い分ける。
target_include_directories(${PROJECT_NAME} INTERFACE
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
コンフィグ時には評価前の文字列でしかないので、
if() などの条件分岐にも使えないし、
cmake_print_variables() や message() しても中身は見えない。
実際にどんな値が入ったかを確かめるには
file(GENERATE OUTPUT <outfile> CONTENT <content>)
などでビルド時に書き出すことになる。
Modules
https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html
include() や find_package() から使う。
include(CMakePrintHelpers)
変数の名前と中身を表示してくれる。 名前を2回書かずに済む。 次の二つは等価:
cmake_print_variables(VAR)
message(STATUS VAR="${VAR}")
GNUInstallDirs
https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html
インストール先のディレクトリを指定するときの標準的な値を決めてくれる。
| Variable | Value |
|---|---|
CMAKE_INSTALL_BINDIR |
bin |
CMAKE_INSTALL_INCLUDEDIR |
include |
CMAKE_INSTALL_LIBDIR |
lib, lib64 |
CMAKE_INSTALL_DATADIR |
share |
CMAKE_INSTALL_FULL_<dir> |
${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_<dir>} |
CMakePackageConfigHelpers
- https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html
- https://cmake.org/cmake/help/latest/guide/importing-exporting/
他のプロジェクトから以下のように利用されるライブラリを作りたい。
外部から IMPORTED されたターゲットであることを明確にするため
名前空間::ターゲット という形でリンクするのが筋:
project(OtherProject CXX)
find_package(MyLib)
target_link_libraries(OtherTarget PRIVATE MyLib::MyLib)
こうやって使ってもらうためには *config.cmake ファイルを
find_package() の探索先
のどこかにインストールする必要がある。
選択肢があり過ぎて悩ましく、公式ドキュメントにも推奨などは書かれていないが、
識者のコメント
によれば次の二択で使い分ける方針が良さそう:
share/cmake/${PROJECT_NAME}: header-only, architecture-independent.find_package()は${CMAKE_INSTALL_DATADIR}に追従せずshareを読みにいく。${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}: コンパイル成果物を含む、architecture-dependent.
install(TARGETS) の EXPORT オプションでターゲットを関連づけ、
install(EXPORT) でファイルのインストールを設定する:
project(MyLib
VERSION 0.1.0
LANGUAGES CXX)
# ...
install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}-config
)
set(config_destination ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(EXPORT ${PROJECT_NAME}-config
DESTINATION ${config_destination}
NAMESPACE ${PROJECT_NAME}::
)
バージョン情報も同じところに送り込む:
set(version_file ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(${version_file}
COMPATIBILITY AnyNewerVersion
)
install(FILES ${version_file}
DESTINATION ${config_destination}
)
find_package() から使うにはここまでの設定で十分だが、
add_subdirectory() からでも同じ形で使えるようにするため、
ALIAS を設定しておいたほうがいい:
add_library(${PROJECT_NAME})
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
上記のように直接 EXPORT *-config するのは簡易版。
ライブラリ利用時に何らかの処理を行いたい場合は、
同じ内容を EXPORT *-targets のような名前で書き出しておき、
それを include() する *-config.cmake を次のようなコマンドで生成する。
configure_package_config_file(config.cmake.in ${PROJECT_NAME}-config.cmake
INSTALL_DESTINATION ${config_destination}
)
鋳型となる config.cmake.in にはとりあえず次の3行を書く:
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/${CMAKE_FIND_PACKAGE_NAME}-targets.cmake")
check_required_components(${CMAKE_FIND_PACKAGE_NAME})
これだけだと直接 EXPORT *-config するのとほとんど変わらないけど、
ほかにも好きな処理を書いてインストールできる、というのがミソ。
例えばzlibを使うライブラリを作って、その依存関係を伝播させたいとき、
次のような処理を書いておくと利用者側で find_package(ZLIB) を書かなくてよくなる:
include(CMakeFindDependencyMacro)
find_dependency(ZLIB)
find_dependency()
は上流で指定された REQUIRED や QUIET などをうまく転送してくれる find_package() ラッパー。
check_required_components() は
configure_package_config_file() が @PACKAGE_INIT@ のところに生成してくれる関数で、
サポート外のコンポーネントが指定された場合に <PackageName>_FOUND をFalseにする。
具体的には、ユーザーから find_package(<PackageName> COMPONENTS compo) とされたときに
<PackageName>_compo_FOUND 変数をチェックするだけなので、
例えば上記の2行に加えて次のように書くと、
zlibが見つかったら zlib コンポーネントを提供する、という挙動になる:
set(${CMAKE_FIND_PACKAGE_NAME}_zlib_FOUND ${ZLIB_FOUND})
check_required_components() はメッセージが不親切だったりしてイマイチなので、
NO_CHECK_REQUIRED_COMPONENTS_MACRO オプションで作らせないようにして、
foreach(component ${${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS})
を自分で回してチェックするパターンもよく見かける。
公式ガイド
でも
pr0g/cmake-examples
でもそうしている。
コンポーネントの提供方法について調べると、 それぞれ異なる使用例みたいなものしか見つからなくて理解に苦労した。 しかし分かってみると要件は案外単純:
*config.cmake内でユーザーから指定されたコンポーネントをチェックし、 メッセージを表示するなり何なりの応答する。選択肢は2つ。<PackageName>_<component>_FOUNDを定義してcheck_required_components()に任せる。${<PackageName>_FIND_COMPONENTS}を自前で回して処理する。
- 慣例的に、コンポーネントと同名のターゲットを名前空間内に定義して
EXPORTする。
FetchContent
https://cmake.org/cmake/help/latest/module/FetchContent.html
外部ライブラリをコンフィグ時に取ってくる。 CMake 3.11 から。
include(FetchContent)
FetchContent_Declare(pcglite
GIT_REPOSITORY https://github.com/heavywatal/pcglite.git
GIT_TAG v0.2.1
EXCLUDE_FROM_ALL
FIND_PACKAGE_ARGS 0.2.1
)
FetchContent_MakeAvailable(pcglite)
cmake_print_variables(pcglite_FOUND pcglite_DIR)
cmake_print_variables(pcglite_SOURCE_DIR pcglite_BINARY_DIR)
FetchContent_Declare()- まずこれで依存関係を宣言する。
- ソースに関するオプションは ExternalProject とほぼ同じで、そちらを見に行く必要がある。
GIT_TAGはタグだけでなくブランチやハッシュも指定できる。 公式推奨はハッシュらしい。GIT_SHALLOWが使えるのはタグとブランチのみ。EXCLUDE_FROM_ALL: (3.28+) findできなかった場合のadd_subdirectory()に渡される。 一緒にインストールする必要のないPRIVATE依存のときに。FIND_PACKAGE_ARGS: (3.24+)FetchContent_MakeAvailable()- 宣言された依存ライブラリを利用可能な状態にする。(3.14+)
- これひとつを実行することが推奨されているが、各段階を手動で書くこともできる。
- (3.24+)
find_package()を試みる。 見つかったら3つの変数をセットして終了:<lowercaseName>_FOUND<lowercaseName>_DIR<lowercaseName>_POPULATED
FetchContent_GetProperties()で過去にPopulateしたものがあるか確認。<lowercaseName>_POPULATEDが定義されていなければ次に進む。FetchContent_Populate()でソースコードを取得する。 成功したら3つの変数をセットする:<lowercaseName>_POPULATED<lowercaseName>_SOURCE_DIR<lowercaseName>_BINARY_DIR
add_subdirectory()でプロジェクトに取り込む。
- (3.24+)
FETCHCONTENT_TRY_FIND_PACKAGE_MODE- (3.24+;
OPT_IN,ALWAYS,NEVER) ソースを取ってくる前にfind_package()で探すかどうかを制御する。 デフォルト未定義はOPT_INと同じで、FIND_PACKAGE_ARGSが(空でも)指定されていたら実行。 FETCHCONTENT_SOURCE_DIR_<uppercaseName>- 指定したソースツリーをあるがままpopulate済みとして使う。
GIT_TAGなども無視。
FETCHCONTENT_SOURCE_DIR_<uppercaseName> が定義されていると
FETCHCONTENT_TRY_FIND_PACKAGE_MODE や FIND_PACKAGE_ARGS によらず
find_package() さえも強制スキップされる謎仕様。
「インストール済み > ローカルに既存のソースツリー > 外部からソース取得」
という節約的な優先順位を実現するのは案外難しい。
同じパッケージの宣言が複数ある場合は早い者勝ちで、階層を超えて共有される。
わかりやすく上位の呼び出し元の情報を優先するため、
先に全部 Declare してからまとめて MakeAvailable するのが推奨。
Projects should aim to declare the details of all dependencies they might use before they call FetchContent_MakeAvailable() for any of them.
似て非なる ExternalProject はビルド時に実行されるので
add_subdirectory() の対象にできず、
execute_process() で git を直接叩くなどして凌いでいた。
Homebrew Formula の中では原則禁止。
できる限り find_package() で済むようにして、
どうしても無理なとき -DHOMEBREW_ALLOW_FETCHCONTENT=ON" を末尾に追加。
FindThreads
https://cmake.org/cmake/help/latest/module/FindThreads.html
-lpthread とか自分で書かない。
find_package(Threads)
target_link_libraries(MyTarget PRIVATE Threads::Threads)
FindBoost
https://cmake.org/cmake/help/latest/module/FindBoost.html
Boostの特別扱いはdeprecatedになった。
普通のCMakeパッケージとして探すには、
CONFIG とか NO_MODULE オプションを足して明示的にConfigモードを使う。
find_package(Boost CONFIG REQUIRED COMPONENTS context)
target_link_libraries(MyTarget PRIVATE Boost::context)
ヘッダーだけでいい場合は Boost::boost をリンクする。
CTest
https://cmake.org/cmake/help/latest/module/CTest.html
include(CTest)
if(BUILD_TESTING)
add_subdirectory(test)
endif()
# test/CMakeLists.txt
add_executable(test-gene gene.cpp)
add_test(NAME gene COMMAND $<TARGET_FILE:test-gene>)
ctest -V で実行。
一部のテストのみ実行したいときは -R <pattern> で絞る。
include(CTest)
は勝手にCDashの設定をして
DartConfiguration.tcl を生成する。
次のように書き換えればそのへんをスキップできる:
option(BUILD_TESTING "Build the testing tree." ON)
enable_testing()
CLI
cmake
https://cmake.org/cmake/help/latest/manual/cmake.1.html
ビルド用のディレクトリを別に作って out-of-source で実行するのが基本。
やり直したいときは、そのディレクトリごと消す。
3.0以降 cmake --target clean はあるが、
CMakeのバージョンを上げたときなどcleanしたい場面で使えない。
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=${HOME}/local -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j2
cmake --install build
-S <dir>- ソースツリーを指定する。
3.13から。それまではundocumentedで
-H<dir>という形だった。 -B <dir>- ビルドツリーを指定する。 3.13から。それまではundocumentedだった。
-D <var>=<value>- コマンドラインから変数を設定する。
-G <generator-name>- Makefile, Ninja, Xcode, etc.
- デフォルトでは
Makefileが書き出されるのでmake && make installと書いてもいいけど、 generator非依存のcmake --buildを使ったほうがいい。 cmake --install <dir>が使えるのは3.15以降。 それまではcmake --build build --target installと明示する必要があった。-E <command>- シェルの違いを気にせず基本的なコマンドが使えるように。e.g.,
chdir <dir> <cmd>make_directory <dir>-L- キャッシュされている変数をリストアップ。
Hをつけると説明文も。Aをつけるとadvancedな変数も。 見るだけなら-Nオプションと共に。
Style guide
https://learn.microsoft.com/en-us/vcpkg/contributing/cmake-guidelines
Versions
- 4.1: Fix
^regex bug - 3.30:
FetchContentwithout sub-build - 3.28:
FetchContent_Declare(... EXCLUDE_FROM_ALL), C++20 modules, Ubuntu 24.04 noble - 3.24:
FetchContent_Declare(... FIND_PACKAGE_ARGS),--fresh - 3.23:
FILE_SET - 3.22: Ubuntu 22.04 jammy
- 3.21:
PROJECT_IS_TOP_LEVEL - 3.20:
cmake_path(),cxx_std_23 - 3.19:
CMakePresets.json - 3.17:
CMAKE_EXPORT_COMPILE_COMMANDS - 3.16: Ubuntu 20.04 focal
- 3.15:
cmake --install - 3.13:
cmake -S . -B build,target_link_directories,target_link_options - 3.12:
cxx_std_20, linkingOBJECTlibraries - 3.11:
include(FetchContent) - 3.8:
cxx_std_17