Edited at

ありきたりなCMakeのプロジェクト作成 for C++


初心者C++Advent Calendar 2017

この記事は初心者C++Advent Calendar 2017 12日目の記事です

<< 11日目|autoの推論まとめ、あるいはautoは忖度などしない件 || 13日目|C++の分割コンパイル時に関数のオーバーロード(void test(int)と void test(double)はどうやって区別されているか? >>

本来もっとあとに記事を出す予定でしたがなんか空いているのでゆるふわ記事を出します。ガチな記事は12/15までお待ち下さい。

まだ初心者C++ AdC、2つ1つ空いてますね。だれかstd::string_viewについて書きませんか??私は時間が足りません。


ありきたりなCMakeのプロジェクト作成

CMakeも極めればだいぶ色々できるスクリプト言語らしく、Findxxx系のスクリプトをのぞくと毎回うわぁとなってしまいます。

がしかし多くのCMakeユーザーに必要なのはそんなガチな書き方じゃなくてもっとゆるふわな、そういう動機のはず。


先行記事

当然CMakeなんて超メジャーツール、日本語の解説がないわけがない。

自分がQiitaでストックしていた記事だけでもこんなものがあった。なかでもごく簡単なcmakeの使い方は極めて簡潔でわかりやすい。


問題点

たしかにごく簡単なcmakeの使い方はわかりやすいんだけど、実際書こうとするともうすこし色々書かないと実用に耐えない。その辺を解説している記事がたどり着きにくく、StackOverflow何かを読まないといけない。


本題

まあまず実際に使っているプロジェクトを見るのが早いだろう。

https://github.com/YSRKEN/KanColleSimulator_KAI/blob/master/KCS_CUI/source/CMakeLists.txt


CMakeLists.txt

cmake_minimum_required(VERSION 3.1)

enable_language(CXX)
set(CMAKE_CXX_STANDARD 14) # C++14...
set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required...
set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11
find_package(Threads REQUIRED)

## Set our project name
project(KCS_CUI)

set(kcs_kai_src
config.cpp
exception.cpp
fleet.cpp
kammusu.cpp
main.cpp
mapdata.cpp
other.cpp
random.cpp
result.cpp
simulator.cpp
weapon.cpp
)
if(MSVC)
# Force to always compile with W4
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
endif()
elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
# Update if necessary
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-long-long -pedantic")
endif()
## Define the executable
add_executable(KCS_CUI ${kcs_kai_src})
target_link_libraries(KCS_CUI Threads::Threads)


解説していく。


まずやること

プロジェクトの名前を決める。普通これがそのまま実行ファイルなりライブラリの名前になる。

これが

project(KCS_CUI)

の部分だ。以降のKCS_CUIの部分はここで決定したものを使う。


ソースファイルの列挙

CMakeLists.txtからみた相対パスでソースファイルを列挙する。

なお#include "xxx.h"のようにincludeされているものについては書かなくてもCMakeがいい感じにしてくれる。つまり翻訳単位となる.cppファイルが該当する。

今回は可読性の観点からkcs_kai_srcという変数に格納した。

set(kcs_kai_src 

config.cpp
exception.cpp
fleet.cpp
kammusu.cpp
main.cpp
mapdata.cpp
other.cpp
random.cpp
result.cpp
simulator.cpp
weapon.cpp
)

CMakeの変数のガバガバさがわかる(違。


C++14でコンパイルする

enable_language(CXX)

set(CMAKE_CXX_STANDARD 14) # C++14...
set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required...
set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11

の部分だ。CMAKE_CXX_STANDARD11とか14とか17とか指定すればいいはずだ。

CMAKE_CXX_EXTENSIONSOFFにしておかないとGNU拡張のよく分からんのが使えてしまう。C++erならそんなことはしないはずだ。


警告レベルの設定

C++erなら、Visual C++なら/W4、GCC/Clangなら-Wall -Wextraくらいはつけて使っているはずだ。

if(MSVC)

# Force to always compile with W4
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
endif()
elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
# Update if necessary
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-long-long -pedantic")
endif()

それがこの部分だ。なおVisual Studio Clang CodeGenについては考慮していない。今回はなんとなく-Wno-long-long -pedanticもつけている。


コンパイル

ではコンパイルコマンドを発行しよう。

add_executable(KCS_CUI ${kcs_kai_src})


C++11 Threadを使う

今回の対象となるソースコードはC++11で追加されたThreadを使っている。こいつを使うには例えばpthreadとリンクしないといけなかったりする。その辺をやっているのが

find_package(Threads REQUIRED)

target_link_libraries(KCS_CUI Threads::Threads)

だ。この合わせて2行でよきに計らってくれる。


ちょっとしたオプションを使いたい時

これも実例を見たほうが早いだろう。

https://github.com/yumetodo/random_generator_iterator/blob/master/CMakeLists.txt


CMakeLists.txt

cmake_minimum_required(VERSION 2.8)

enable_language(CXX)
set(CMAKE_CXX_STANDARD 14) # C++14...
set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required...
set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11

## Use the variable PROJECT_NAME for changing the target name
set( PROJECT_NAME "random_generator_iterator" )

## Set our project name
project(${PROJECT_NAME})

set(SRCS "main.cpp")

if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
# Update if necessary
option(USE_RDRND "enable rdrnd" 1)
option(USE_RDSEED "enable rdseed" 0)
if(USE_RDRND)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mrdrnd")
endif()
if(USE_RDSEED)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mrdseed")
endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
endif()

## Define the executable
add_executable(${PROJECT_NAME} ${SRCS})


特に難しいことはない。オプションに文字列を指定させたい場合以外は単にoptionを呼び出せばいい。第一引数が変数名でそれを見て適宜条件分岐すればいい。


Findxxx系をつかう: BoostとOpenSSL/LibreSSLを例に

次に示すCMakeは書いたときは付属してくるFindBoost.cmakeにバグが有ってワークアラウンドとしてレポジトリにパッチを当てたものを置いており、CMakeのバージョンを見て切り替えている。

なおどういうわけかLibreSSLだとCMake側はいいのだがリンクでコケる。なんでや

https://github.com/yumetodo/boost_asio_ssl_file_dl_test/blob/support_libressl/CMakeLists.txt


CMakeLists.txt

cmake_minimum_required(VERSION 3.7.2)

enable_language(CXX)
set(CMAKE_CXX_STANDARD 14) # C++14...
set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required...
set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11

#
# Config options
#
option(BOOST_ASIO_SSL_FILE_DL_TEST_USE_LIBRE_SSL "Use LibreSSL instead of OpenSSL" TRUE)

#
# Set our project name
#
project(boost_asio_ssl_file_dl_test)

#
# Source files
#
set(boost_asio_ssl_file_dl_test_src
boost_asio_ssl_file_dl_test/Source.cpp
boost_asio_ssl_file_dl_test/downloader.cpp
boost_asio_ssl_file_dl_test/downloader_impl.cpp
)

#
# find libraries and include
#

# find thread library
find_package(Threads REQUIRED)

# find Boost
# https://cmake.org/cmake/help/v3.7/module/FindBoost.html
if(${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION} LESS 3.8)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules)
include(FindBoost)
message(STATUS "FindBoost: commit 9a8881c975ba8d814c39d52c370f3277afa80fda is used.")
endif()
set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_DEBUG_RUNTIME OFF)
set(Boost_DEBUG ON)
add_definitions(${Boost_LIB_DIAGNOSTIC_DEFINITIONS})
set(boost_required_components
system date_time regex zlib bzip2 iostreams
# thread
)
find_package(Boost 1.59 REQUIRED COMPONENTS ${boost_required_components})
if(NOT Boost_FOUND)
# Config options
set(BOOST_ROOT ${BOOST_ROOT} CACHE PATH "Set boost root directory" FORCE)
set(BOOST_LIBRARYDIR ${BOOST_LIBRARYDIR} CACHE PATH "Set boost library directory" FORCE)
message(FATAL_ERROR "Fail to find Boost")
endif()
include_directories(${Boost_INCLUDE_DIRS})

# find SSL Library
if(BOOST_ASIO_SSL_FILE_DL_TEST_USE_LIBRE_SSL)
# find LibreSSL
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules)
include(FindLibreSSL)
find_package(LibreSSL)
if(NOT LIBRESSL_FOUND)
message(FATAL_ERROR "Fail to find LibreSSL") # exit
endif()
message(STATUS "LibreSSL_INCLUDE_DIR: ${LibreSSL_INCLUDE_DIR}")
include_directories(${LibreSSL_INCLUDE_DIR})
else()
# find OpenSSL
# https://cmake.org/cmake/help/v3.7/module/FindOpenSSL.html
find_package(OpenSSL REQUIRED)
if(NOT OPENSSL_FOUND)
message(FATAL_ERROR "Fail to find OpenSSL") # exit
endif()
message(STATUS "OPENSSL_INCLUDE_DIR: ${OPENSSL_INCLUDE_DIR}")
include_directories(${OPENSSL_INCLUDE_DIR})
endif()

#
# Update compiler waring flags
#
if(MSVC)
# Force to always compile with W4
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
endif()
elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
# Update if necessary
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-long-long -pedantic")
endif()

#
# Compile
#
add_executable(boost_asio_ssl_file_dl_test ${boost_asio_ssl_file_dl_test_src})

#
# Link
#
target_link_libraries(boost_asio_ssl_file_dl_test Threads::Threads)
target_link_libraries(boost_asio_ssl_file_dl_test Boost::disable_autolinking ${Boost_LIBRARIES})
if(BOOST_ASIO_SSL_FILE_DL_TEST_USE_LIBRE_SSL)
target_link_libraries(boost_asio_ssl_file_dl_test ${LibreSSL_LIBRARIES})
else()
target_link_libraries(boost_asio_ssl_file_dl_test OpenSSL::SSL)
target_link_libraries(boost_asio_ssl_file_dl_test OpenSSL::Crypto)
endif()


基本的にFindxx.cmakeのたぐいはファイルを見に行くとコメントで使い方をちゃんと書いてくれているのでそれに従えばいい。というよりそれぞれ微妙に作法が違うので統一的な説明ができない。


少しづつ設定を変えて同じソースコードをビルドしたいとき

ベンチマークなどで、マクロの定義を差し替えて同じソースコードから複数のbinaryを吐きたいことがある。実例を見よう。

https://github.com/yumetodo/qsort

directory構成は

/project root

├── benchmark.sh
├── CMakeLists.txt
├── main_prog.cpp
├── mm88.h
├── mm88c.c
├── qs10a5.c
├── qs10a5.h
├── qs9e17.c
├── qs9e17.h
├── random_device.hpp
├── qs10a5
│ └── CMakeLists.txt
├── qs10a5m
│ └── CMakeLists.txt
├── qs9e17
│ └── CMakeLists.txt
└── qsort
├── CMakeLists.txt
└── main.cpp

こんな感じです。この場合以下のように記述するとルートディレクトリでcmakeすると全部まとめてビルドされます.


/CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

enable_language(CXX)
set(CMAKE_CXX_STANDARD 14) # C++14...
set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required...
set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11

#
# Update compiler waring flags
#
if(MSVC)
# Force to always compile with W4
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
endif()
elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
# Update if necessary
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-long-long -pedantic")
endif()

add_subdirectory(qsort)
add_subdirectory(qs9e17)
add_subdirectory(qs10a5)
add_subdirectory(qs10a5m)

install(
FILES "./benchmark.sh"
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
DESTINATION "."
)



/qs10a5/CMakeLists.txt

set(

SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/../main_prog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../mm88c.c"
"${CMAKE_CURRENT_SOURCE_DIR}/../qs10a5.c"
)
add_definitions(-DUSE_QS10A5)
add_executable(qs10a5 ${SRCS})

install(TARGETS qs10a5 RUNTIME DESTINATION ".")



/qs10a5m/CMakeLists.txt

set(

SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/../main_prog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../mm88c.c"
"${CMAKE_CURRENT_SOURCE_DIR}/../qs10a5.c"
)
add_definitions(-DUSE_QS10A5 -DMEMCPY)
add_executable(qs10a5m ${SRCS})

install(TARGETS qs10a5m RUNTIME DESTINATION ".")



/qs9e17/CMakeLists.txt

set(

SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/../main_prog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../mm88c.c"
"${CMAKE_CURRENT_SOURCE_DIR}/../qs9e17.c"
)
add_definitions(-DUSE_QS9E17)
add_executable(qs9e17 ${SRCS})

install(TARGETS qs9e17 RUNTIME DESTINATION ".")



/qsort/CMakeLists.txt

set(

SRCS
"${CMAKE_CURRENT_SOURCE_DIR}/../main_prog.cpp"
)
add_definitions(-DUSE_STDLIB_QSORT)
add_executable(qsort ${SRCS})

install(TARGETS qsort RUNTIME DESTINATION ".")


$ mkdir build

$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX="../bin" -DCMAKE_BUILD_TYPE=Release ..
$ make install
$ cd ../bin


NatvisファイルをVSのプロジェクトに追加したいとき

Visual Studioのデバッガでの表示をカスタマイズするものとしてNatvisがあるわけですが、

Create custom views of native objects in the debugger | Microsoft Docs

.vcxprojでそれを追加して認識させるためには

  <ItemGroup>

<Natvis Include="inferior_vector.natvis" />
</ItemGroup>

のようなものが書かれていれば認識されます。ではCMakeからこれを吐き出すには?

add_executable(<project name> <source files>... <natvis files>...)

単にadd_executableに指定してあげればいいようです(Thanks @Chironian )。順番はどうでもいいです。

VS以外には単に無視されるのでif(MSVC)とか書く必要はないです。

ちなみにCMake3.7.0からなので、

cmake_minimum_required(VERSION 3.7.0)

しましょう。


CMakeの、もしくは付属のFindxxx.cmakeにバグを見つけた時

Let's contribute!

GithubにもCMakeのレポジトリがありますが、ただの案山子なのでGitLabのほうを見に行きましょう。

https://gitlab.kitware.com/cmake/cmake/blob/master/CONTRIBUTING.rst

このへんよく読んでMerge Requestを投げつけましょう。ローカルにgit hock入れさせられます。

自分が過去に投げてMergeされたやつ↓

FindBoost: Fix release name candidate list construction (!673) · Merge Requests · CMake / CMake · GitLab

C#周りはまだバグが多そうな印象なので(適当)チャンスですね。

@Chironian 氏が先月あたり

CSharp: fix generation fail in environment except English (!1449) · Merge Requests · CMake / CMake · GitLab

投げてましたね。


CMakeでライブラリをビルドするとき

Findxxx系を作って提供するのは時代遅れになりつつあり、xxx-config.cmakeを自動生成させるようにするべきです。

このときcmake2.8.12以降を使い、またcmake_minimum_requiredもそのようにしましょう(さもないとCMP0022が出る)。

詳細は

お手軽な xxx-config.cmake の作成方法

がよくまとまっているので丸投げします。


License

CC BY 4.0