Heavy Watal

CMake — Cross-platform Make

autotoolsconfigure 的な位置づけで、 環境に合わせた Makefile を自動生成する。

https://cmake.org/cmake/help/latest/

基本

CMakeLists.txt を各ディレクトリに配置して、階層的に管理する。 プロジェクトのトップに置くものは、以下のようなコマンドで始める必要がある。

cmake_minimum_required(VERSION 3.1)
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 -Wpedantic)
install(TARGETS a.out
  RUNTIME DESTINATION bin
)

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

Commands

Scripting commands

Project commands

サブディレクトリを利用する:

ターゲットを定義する:

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

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

オプション

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

Variables

Variables that Provide Information

Variables that Change Behavior

Variables that Describe the System

APPLE, UNIX, WIN32

Variables that Control the Build

C++

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_compile_options(-march=native -Wall -Wextra -Wpedantic)

if (NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release)
endif()
message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")
set(CMAKE_CXX_FLAGS_DEV "-O2 -g")
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

文脈に応じて変数を評価する仕組み。

プロジェクト内のビルド時と、外部パッケージとして利用される時とで、 インクルードパスを使い分ける。

target_include_directories(${PROJECT_NAME} INTERFACE
  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Modules

include()find_package() から使う。

GNUInstallDirs

インストール先のディレクトリを指定するときの標準的な値を決めてくれる。

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

Linuxでは lib64 が標準的だが、Linuxbrewで使うには lib にする必要がある。

if(${CMAKE_INSTALL_PREFIX} MATCHES linuxbrew)
  set(CMAKE_INSTALL_LIBDIR lib)
endif()

CMakePackageConfigHelpers

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

project(otherproject CXX)
find_package(mylib)
# add_subdirectory(mylib)
target_link_libraries(othertarget PRIVATE mylib::mylib)

${CMAKE_INSTALL_PREFIX}/share/ らへんにconfigファイルを送り込む必要がある。 install(TARGETS) の中で EXPORT のためのターゲット定義し、 install(EXPORT) でその設定を行う:

project(mylib
  VERSION 0.1.0
  LANGUAGES CXX)
# ...
install(TARGETS ${PROJECT_NAME}
  EXPORT ${PROJECT_NAME}-config
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(EXPORT ${PROJECT_NAME}-config
  DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}
  NAMESPACE ${PROJECT_NAME}::
)

バージョン情報も同じところに送り込む:

set(VERSION_CONFIG ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
  ${VERSION_CONFIG} COMPATIBILITY AnyNewerVersion
)
install(FILES ${VERSION_CONFIG}
  DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}
)

find_package() から使うにはここまでの設定で十分だが、 add_subdirectory() からでも同じ形で使えるようにするため、 ALIAS を設定しておいたほうがいい:

add_library(${PROJECT_NAME} SHARED)
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})

FetchContent

外部ライブラリを取ってきて配置する。 前からあった ExternalProject はビルド時に実行されるため add_subdirectory() の対象にできないなどの問題があったが、 こちらはコンフィグ時に実行される。

CMake 3.11 からの新機能なので、もう少し普及するまでお預け。 当面は execute_process() で凌ぐ:

find_package(Git)
execute_process(COMMAND
  ${GIT_EXECUTABLE} clone --recursive --depth=1 --branch=master https://github.com/USER/REPO.git ${SUBDIR}
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_subdirectory(${SUBDIR} EXCLUDE_FROM_ALL)

FindThreads

-lpthread とか自分で書かない。

find_package(Threads)
target_link_libraries(mytarget PRIVATE Threads::Threads)

FindBoost

set(Boost_NO_BOOST_CMAKE ON)
find_package(Boost REQUIRED COMPONENTS filesystem)
target_link_libraries(mytarget PRIVATE Boost::filesystem)

探索パスを追加するには BOOST_ROOT を設定する。

CTest

include(CTest)
if(BUILD_TESTING)
  add_subdirectory(test)
endif()

enable_testing() と書くほうが短いけど BUILD_TESTING=ON 固定なのでオプションで切れない。

# test/CMakeLists.txt
add_executable(test-gene gene.cpp)
add_test(NAME gene COMMAND $<TARGET_FILE:test-gene>)

ctest -V で実行。 一部のテストのみ実行したいときは -R <pattern> で絞る。

CLI

cmake

いろんな中間ファイルができる上に cmake clean は無いので、 ビルド用の空ディレクトリを外に作って out-of-source で実行するのが基本。 やり直したいときは、そのディレクトリごと消す。

mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=${HOME}/local -DCMAKE_BUILD_TYPE=Debug /path/to/project

デフォルトでは Makefile が書き出されるので make && make install のように実行してもいいけど、 cmake からそれを実行することもできる:

cmake --build . -- -j2
cmake --build . --target install

オプション

-DCMAKE_XXX=YYY
コマンドラインから変数を設定する。
-G <generator-name>
Makefile, Ninja, Xcode, etc.
-E <subcommand>
chdir <dir> <cmd>
make_directory <dir>
-H <dir> (undocumented)
ソースツリーを指定する。
-B <dir> (undocumented)
ビルドツリーを指定する。
-L
キャッシュされている変数をリストアップ。 H をつけると説明文も。 A をつけるとadvancedな変数も。 見るだけなら -N オプションと共に。