Heavy Watal

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)

cmake コマンドの使い方は後述

Commands

https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html

Scripting commands

Project commands

https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html#project-commands

外部のCMakeプロジェクトを利用する:

ターゲットを定義する:

ターゲットのプロパティを追加する:

インストールするものや宛先を指定する。

オプション

PRIVATE
このターゲットをビルドするときだけ使い、これを利用するときには参照させない。 例えば「このプロジェクトのライブラリをビルドするにはBoostヘッダーが必要だけど、 これを利用するときにそれらのパスを知る必要はない」とか。
INTERFACE
このターゲットでは使わないけど、これを利用するときには参照させる。 例えば、ヘッダーライブラリを作る場合とか。
PUBLIC
このターゲットにもこれを利用するターゲットにも使う。使う場面あるかな?
EXCLUDE_FROM_ALL
make [all] から外れて、明示的なターゲット指定でのみビルドされるようになる。

Variables

https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html

Variables that Provide Information

Variables that Change Behavior

Variables that Describe the System

APPLE, UNIX, WIN32

Variables that Control the Build

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++

target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF)
target_compile_options(common PRIVATE
  -Wall -Wextra -pedantic
  $<$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},x86_64>:-march=native>
  $<$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},arm64>:-march=armv8.3-a+sha3>
)

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release)
endif()
cmake_print_variables(CMAKE_BUILD_TYPE)
set(CMAKE_CXX_FLAGS_DEV "-O2 -g")

CMAKE_CXX_* のようなグローバル設定を使わず target_*() でターゲットごとに設定するのが今後の主流。 CMAKE_CXX_KNOWN_FEATUREScxx_std_17 などの便利なメタタグが導入されたのは CMake 3.8 から。

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 は用意されていないので自分で定義する。 CMAKE_CXX_FLAGS_??? を適当に作れば -DCMAKE_BUILD_TYPE=??? をcase-insensitiveに解釈してもらえる。

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

他のプロジェクトから以下のように利用されるライブラリを作りたい。 外部から IMPORTED されたターゲットであることを明確にするため 名前空間::ターゲット という形でリンクするのが筋:

project(OtherProject CXX)
find_package(MyLib)
target_link_libraries(OtherTarget PRIVATE MyLib::MyLib)

こうやって使ってもらうためには *config.cmake ファイルを find_package() の探索先 のどこかにインストールする必要がある。 選択肢があり過ぎて悩ましく、公式ドキュメントにも推奨などは書かれていないが、 識者のコメント によれば次の二択で使い分ける方針が良さそう:

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() は上流で指定された REQUIREDQUIET などをうまく転送してくれる 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 でもそうしている。

コンポーネントの提供方法について調べると、 それぞれ異なる使用例みたいなものしか見つからなくて理解に苦労した。 しかし分かってみると要件は案外単純:

FetchContent

https://cmake.org/cmake/help/latest/module/FetchContent.html

外部ライブラリをコンフィグ時に取ってくる。 CMake 3.11 から。

include(FetchContent)
set(FETCHCONTENT_QUIET OFF)
cmake_print_variables(FETCHCONTENT_SOURCE_DIR_IGRAPH)
FetchContent_Declare(
  igraph
  GIT_REPOSITORY https://github.com/igraph/igraph.git
  GIT_TAG ${PROJECT_VERSION}
  GIT_SHALLOW ON
)
FetchContent_MakeAvailable(igraph)
cmake_print_variables(igraph_SOURCE_DIR, igraph_BINARY_DIR)
FetchContent_Declare()
まずこれで依存関係を宣言する。 複数ある場合、先に全部宣言してからまとめてMakeAvailableを呼ぶのが推奨。
ソースに関するオプションは ExternalProject とほぼ同じ。
FIND_PACKAGE_ARGS: (3.24+)
EXCLUDE_FROM_ALL: (3.28+)
FetchContent_MakeAvailable()
宣言された依存ライブラリを利用可能な状態にする。(3.14+)
これひとつを実行することが推奨されているが、各段階を手動で書くこともできる。
  1. find_package() を試みて、見つからなければ次に進む。(3.24+)
  2. FetchContent_GetProperties() で過去にPopulateしたものがあるか確認。 <lowercaseName>_POPULATED が定義されていなければ次に進む。
  3. FetchContent_Populate() でソースコードを取得する。 FETCHCONTENT_SOURCE_DIR_<uppercaseName> が定義されている場合はfetchせずそこにあるものを使う。 成功したら3つの変数をセットする:
    • <lowercaseName>_POPULATED
    • <lowercaseName>_SOURCE_DIR
    • <lowercaseName>_BINARY_DIR
  4. add_subdirectory() でプロジェクトに取り込む。

似て非なる ExternalProject はビルド時に実行されるので add_subdirectory() の対象にできず、 execute_process() で git を直接叩くなどして凌いでいた。

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 オプションと共に。

Versions