メモの日々


2020年01月04日(土) [長年日記]

[dev][c++] CMakeを使う

CMakeについてメモ。

ドキュメント

リファレンス。

ガイド的な文書。

基本

C++のコードをCMakeでビルドする例を示す。ファイル構成は次の通りとする。

.
├── CMakeLists.txt
├── src
│   ├── CMakeLists.txt
│   ├── hello.cpp
│   ├── hello.h
│   └── main.cpp
└── test
    ├── CMakeLists.txt
    ├── main.cpp
    └── test_hello.cpp

testのビルドは後回しとして、まずsrc配下のプログラムをビルドできるようにする。CMakeLists.txtとsrc/CMakeLists.txtをそれぞれ次のように書く。

cmake_minimum_required(VERSION 2.8)

add_subdirectory(src)
add_executable(hello
  main.cpp
  hello.cpp
)
  • cmake_minimum_requiredは書かないと警告が出るので書く。2.8かかなり昔のバージョンだがCentOS 7のCMakeはこのバージョン。
  • add_subdirectoryでsrc配下のCMakeLists.txtを読み込むことを指示する。
  • add_executableでビルドする実行ファイル名とそれをビルドするためのソースファイル群を指定する。ここにヘッダファイルも記述すべきかはよくわからない。ヘッダファイルを含めないとIDEを使った場合にヘッダファイルが表示されないかもしれないが、ビルドするだけならヘッダファイルを含める必要はない。
    • add_executableにソースファイルを列挙するのはできれば避けたい。fileのGLOB_RECURSEを使うと手動での列挙を避けられるが、これを使うとソースファイルの増減時に手動でのCMakeの再実行が必要になり運用が面倒になる。GLOBはリファレンスマニュアルに「We do not recommend using GLOB to collect a list of source files from your source tree. 」と言及されており、また上でリンクしたEffective Modern CMakeにも「Don't use file(GLOB) in projects.」とあり使用が推奨されていない模様。
    • file(GLOB)にはCONFIGURE_DEPENDSというオプションがある。これはCMake 3.12で追加されておりfile(GLOB)の問題を一部解決するものみたいなのだけれど、試せていない。

CMakeLists.txtとソースコードを用意したら、cmakeコマンドを実行するとビルド環境(LinuxならMakefile)が作られる。

$ mkdir build
$ cd build
$ cmake ..

ビルドを行うにはビルド環境にて--buildオプション付きのcmakeコマンドを実行する。

$ cd build
$ VERBOSE=1 cmake --build .

これで build/src/hello が作られる。環境変数VERBOSEをセットするとビルド時のコマンドが出力されるようになるので指定している。CMake 3.14で--verboseオプションが追加されており、これを使うと環境変数は不要なのかもしれないが試せていない。

cmake --buildはCMakeLists.txtが更新されているとビルド環境の再構築(上記の「cmake ..」相当)もしてくれる。

ビルドタイプの切り替え

ビルド環境構築時にcmakeコマンドにCMAKE_BUILD_TYPEを指定することでコンパイルオプションを切り替えることができる。

$ cmake .. -DCMAKE_BUILD_TYPE=Debug

手元のCMake 2.8だとCMAKE_BUILD_TYPEを切り替えることでGCCのコンパイルオプションが次のように変わる。

未指定オプションなし
Debug-g
Release-O3 -DNDEBUG
RelWithDebInfo-O2 -g -DNDEBUG
MinSizeRel-Os -DNDEBUG

コンパイルオプションの明示的な指定

コンパイルオプションをカスタマイズしたいときにはtarget_compile_optionsを使う。例えば src/CMakeLists.txt に次のように追記する。

target_compile_options(hello
  PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/source-charset:utf-8>
  PRIVATE $<$<CXX_COMPILER_ID:GNU>:-Wall>
  PRIVATE $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Debug>>:-ggdb3>
)
  • 条件に応じて異なるオプションを指定するためにgenerator-expressionsを使用している。
  • Visual C++に対しては/source-chasetオプションを使用してソースコードの文字コードがUTF-8であることを通知している。
  • GCCに対しては、CMAKE_BUILD_TYPEがDebugの場合にオプションを追加するようにしている。
  • target_compile_optionsで指定したオプションは、CMAKE_BUILD_TYPEにより付与されるオプションより後ろに指定される。

インクルードディレクトリとリンクするライブラリの指定

インクルードディレクトリはtarget_include_directories、リンクするライブラリはtarget_link_librariesで指定できる。

Boost.Logを使用する例を記す。

src/CMakeLists.txt に次を追記する。

find_package(Boost 1.71 REQUIRED COMPONENTS log thread)
find_package(Threads REQUIRED)

target_include_directories(hello
  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
  PRIVATE ${Boost_INCLUDE_DIR}
)
target_link_libraries(hello
  PRIVATE ${Boost_LIBRARIES}
  PRIVATE ${CMAKE_THREAD_LIBS_INIT}
)
  • find_package(Boost)でBoostを見つけさせる。また、ライブラリとしてlibboost_logとlibboost_threadを使うことを指示している(libboost_threadを指定しているのはlibboost_logが必要としているため。CMakeのバージョンが高い場合はthreadを明記しなくてもこの依存関係は自動的に解決される)。
  • libboost_threadをリンクするにはスレッドライブラリが必要になるため、find_package(Threads)で見つけさせる。これもCMakeのバージョンが高い場合は明記しなくて大丈夫そう。
  • target_include_directoriesでインクルードディレクトリを指定する。find_package(Boost)が変数Boost_INCLUDE_DIRにBoostのインクルードディレクトリを設定するのでそれを使っている。
  • target_link_librariesでリンクするライブラリを指定する。find_packageが変数Boost_LIBRARIESと変数CMAKE_THREAD_LIBS_INITを設定するのでそれを使っている。CMakeのバージョンが高い場合は ${CMAKE_THREAD_LIBS_INIT} と書く代わりに Threads::Threads と書ける。

また、本質的な記述ではないが、CMakeLists.txt の方には次を追記している。add_subdirectory()より上に書く必要がある。

set(Boost_USE_STATIC_LIBS ON)
set(Boost_NO_BOOST_CMAKE ON)
  • Boost_USE_STATIC_LIBSはBoostのライブラリを静的に(libboost_log.aを)リンクするためのもの。動的リンクする場合は不要。
  • Boost_NO_BOOST_CMAKEはBoostに添付されているCMake設定ファイルを読み込まないようにするもの。手元の環境ではこうしないとfind_package(Boost)が失敗してしまうために指定した。

テストのビルド

後回しにしていたtestディレクトリ配下のビルドをできるようにする。まず CMakeLists.txt に次を追記する。

enable_testing()
add_subdirectory(test EXCLUDE_FROM_ALL)
  • enable_testingはビルドするだけなら不要だが、CMakeを通じてテストの実行も行うのなら必要になる。Visual Studioを使うならこれでVisual Studioのテスト機能と連携するようになるので便利。
  • add_subdirectoryに指定しているEXCLUDE_FROM_ALLは、デフォルトのビルド時にはテストのビルドを行わないようにするため。これによりtest配下のコードはビルドターゲットを明示的に指定した時だけビルドされるようになる。

test/CMakeLists.txtは次のように書く。src/CMakeLists.txtとそれほど変わらない。

find_package(Boost 1.71 REQUIRED COMPONENTS unit_test_framework)

add_executable(hello-test
  main.cpp
  test_hello.cpp
  ../src/hello.cpp
)
target_compile_options(hello-test
  PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/source-charset:utf-8>
  PRIVATE $<$<CXX_COMPILER_ID:GNU>:-Wall>
  PRIVATE $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Debug>>:-ggdb3>
)
target_include_directories(hello-test
  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src
  PRIVATE ${Boost_INCLUDE_DIR}
)
target_link_libraries(hello-test
  PRIVATE ${Boost_LIBRARIES}
)

add_test(NAME test-hello
  COMMAND hello-test --log_level=test_suite --report_level=short
)
  • テストにBoost.Testを使ったのでfind_packageによりlibboost_unit_test_frameworkを検索している。
  • add_testでテスト名と実行するコマンドを指定する。ここではコマンドとしてadd_executableによりビルドした実行ファイルを指定している。log_levelなどはBoost.Testの方のオプション。

上述の通りtestディレクトリにはEXCLUDE_FROM_ALLを指定しているので、テストをビルドするには次のようにターゲットを明示する必要がある。

$ cd build
$ VERBOSE=1 cmake --build . --target hello-test

テストの実行にはctestコマンドが使えるが、特にこのコマンドを使う必要はない気がする。

インストールの設定

ビルドされた実行ファイルを既定のディレクトリへインストールする設定を追加する。src/CMakeLists.txtに次を追記する。

install(TARGETS hello RUNTIME DESTINATION bin)

これで、生成されるMakefileにインストール用のターゲット(install, install/strip, install/local)が追加される。上のように書くと、インストール先は /usr/local/bin 配下になる。

$ make -C build install

CMake 3.15でcmakeコマンドに--installオプションが追加されているので、このバージョン以降ならインストールにもcmakeコマンドが使えるみたい。

ビルド用のMakefileを用意する

cmakeコマンドの発行は色々面倒なので、Makefileを用意したくなる。次のような感じ。

SHELL := /bin/bash
CMAKEFLAGS := -DCMAKE_BUILD_TYPE=Release

build_release_dir := build-release
build_debug_dir := build-debug

.PHONY: release
release: $(build_release_dir)/Makefile
	cd $(<D); VERBOSE=1 cmake --build .

.PHONY: debug
debug: CMAKEFLAGS := -DCMAKE_BUILD_TYPE=Debug
debug: $(build_debug_dir)/Makefile
	cd $(<D); VERBOSE=1 cmake --build .

.PHONY: test
test: CMAKEFLAGS := -DCMAKE_BUILD_TYPE=Debug
test: $(build_debug_dir)/Makefile
	cd $(<D); VERBOSE=1 cmake --build . --target hello-test
	$(<D)/test/hello-test --log_level=test_suite --report_level=short

.PHONY: install
install: release
	$(MAKE) -C $(build_release_dir) install/strip

.PHONY: clean
clean:
	rm -rf $(build_release_dir) $(build_debug_dir)

$(build_release_dir)/Makefile $(build_debug_dir)/Makefile:
	mkdir -p $(@D)
	cd $(@D); cmake .. $(CMAKEFLAGS)

最終的な状態

最終的なCMakeList.txtは次の通り。

cmake_minimum_required(VERSION 2.8)

set(Boost_USE_STATIC_LIBS ON)
set(Boost_NO_BOOST_CMAKE ON)

add_subdirectory(src)

enable_testing()
add_subdirectory(test EXCLUDE_FROM_ALL)

最終的な src/CMakeList.txt は次の通り(行の順序は入れ替えた)。

find_package(Boost 1.71 REQUIRED COMPONENTS log thread)
find_package(Threads REQUIRED)

add_executable(hello
  main.cpp
  hello.cpp)
target_compile_options(hello
  PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/source-charset:utf-8>
  PRIVATE $<$<CXX_COMPILER_ID:GNU>:-Wall>
  PRIVATE $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Debug>>:-ggdb3>
)
target_include_directories(hello
  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
  PRIVATE ${Boost_INCLUDE_DIR}
)
target_link_libraries(hello
  PRIVATE ${Boost_LIBRARIES}
  PRIVATE ${CMAKE_THREAD_LIBS_INIT}
)

install(TARGETS hello RUNTIME DESTINATION bin)

test/CMakeList.txtは上述のまま。