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
  RUNTIME DESTINATION bin
)

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

C++

target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
set_target_properties(${PROJECT_NAME} PROPERTIES
  CXX_STANDARD_REQUIRED ON
  CXX_EXTENSIONS OFF
  POSITION_INDEPENDENT_CODE ON
  WINDOWS_EXPORT_ALL_SYMBOLS ON
)
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>}

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

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

CMakePackageConfigHelpers

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

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

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

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

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

ヘッダーだけでいい場合は Boost::boost ターゲットをリンクする。

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

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だった。
-DCMAKE_XXX=YYY
コマンドラインから変数を設定する。
-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