はじめに
8年ぶりにC++で開発することになり、慣れるために個人プロジェクトで簡単なCLIツールを作ってみました。
このとき一番苦労したのが、CMakeLists.txtの設計。
タイトルにある「CLIツールのためのCMakeLists」はまさに自分が探していたもの。
開発中に参考になるものがないか、Qiitaの記事などを物色してました。
結論から言えば、CMakeの概念を理解すると、CLIツール特有の設計とかはないことが分かります。自分と同じような悩みをもつ人の一助になればと思い、このタイトルになっています。
プロジェクトの構造
プロジェクト全体の構造は以下のようになります。
ディレクトリ構造はChatGPTに聞いた「オーソドックスなC++プロジェクトの構造」を参考にしています。
.
├── CMakeLists.txt
├── LICENSE
├── README.md
├── cmake
│ ├── CLI11.cmake
│ ├── cpr.cmake
│ └── gtest.cmake
├── include
│ └── fx
│ ├── Currency.hpp
│ ├── Formatter.hpp
│ ├── Fx.hpp
│ └── FxInterface.hpp
├── src
│ ├── CMakeLists.txt
│ ├── Currency.cpp
│ ├── Formatter.cpp
│ ├── Fx.cpp
│ ├── FxInterface.cpp
│ └── main.cpp
└── tests
├── CMakeLists.txt
├── include
│ └── mock
└── src
├── CurrencyTest.cpp
├── FormatterTest.cpp
├── FxInterfaceTest.cpp
├── FxTest.cpp
└── main.cpp
先人たちの記事を参考にして、各ディレクトリにCMakeLists.txtを作っています。
Web系の開発を長くやっていたので、複数のCMakeLists.txtを作ることやそれぞれのCMakeLists.txtの違いなど最初は意味が分からなかったです。
基本構成
プロジェクトルート直下のCMakeLists.txtです。
できるだけシンプルにするため、最新バージョンのものから少し削っています。
もっとシンプルな実例なら、cprのexampleコードのCMakeLists.txtを参考にするのがおススメです。
https://github.com/libcpr/example-cmake-fetch-content/blob/main/CMakeLists.txt
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16.3)
# it enable to refer project name as ${PROJECT_NAME}
project(fx
VERSION 1.0
DESCRIPTION "CLI tool of converting number to foreign country style"
LANGUAGES CXX)
# C++ version
set(CMAKE_CXX_STANDARD 17)
# output error when specified C++ version is unavailable
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ...without compiler extensions like gnu++11
set(CMAKE_CXX_EXTENSIONS OFF)
add_subdirectory(src)
if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
message(FAITAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt.")
endif()
# define the executable
add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE libfx)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include)
# set compiler options
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -pedantic)
# install
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
前半の部分はプロジェクト名の設定とかコンパイラーのバージョンやパラメータの設定などです。この辺りは先人たちのCMakeLists.txtを参考にして作りました。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16.3)
# it enable to refer project name as ${PROJECT_NAME}
project(fx
VERSION 1.0
DESCRIPTION "CLI tool of converting number"
LANGUAGES CXX)
# C++ version
set(CMAKE_CXX_STANDARD 17)
# output error when specified C++ version is unavailable
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ...without compiler extensions like gnu++11
set(CMAKE_CXX_EXTENSIONS OFF)
次のポイントは必要なファイルやディレクトリへの参照を記述することです。
# CMakeLists.txt
# srcディレクトリの追加
add_subdirectory(src)
# ヘッダーファイルを格納するincludeディレクトリはtarget_include_directories()で追加する
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include)
最後に実行ファイルの作成です。
# CMakeLists.txt
add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE libfx)
main.cppのみを実行対象として登録します。
後述するユニットテストのことなどを考慮しないならば、以下のように全ての.cppファイルをadd_executable()で登録することもできます。
add_executable(${PROJECT_NAME}
src/main.cpp
src/Fx.cpp
src/FxInterface.cpp
src/Formatter.cpp
src/Currency.cpp)
今回はmain.cpp以外はライブラリlibxxx(このプロジェクトの場合はlibfx)としてまとめて、target_link_libraries()で紐づけています。
libfxはsrcディレクトリ以下のCMakeLists.txtに記述しています。
# src/CMakeLists.txt
# add library
add_library(libfx
Fx.cpp
FxInterface.cpp
Currency.cpp
Formatter.cpp)
# set include directory
target_include_directories(libfx PUBLIC ${CMAKE_SOURCE_DIR}/include)
これはlibfxをユニットテストのコードと紐づけて、実行できるようにするためです。
リンク時の依存関係はPRIVATEを指定して、不用意に伝播しないようにしています。
外部ライブラリのインストール
外部ライブラリのインストール方法は、たいていそのライブラリのGithubページに書いてあるので、それを確認しましょう。しかし、だいたい以下のようになります。
# cmake/CLI11.cmake
include(FetchContent)
FetchContent_Declare(
CLI11
GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
GIT_TAG v2.4.2
)
FetchContent_MakeAvailable(CLI11)
git submoduleで予めレポジトリに紐づけておく方法もある(そちらの方がビルドが速いのかも)のですが、ここでは完全にCMakeのみによってライブラリをインストールするようにしています。
また、これらを一つのCMakeLists.txtにまとめて書いていると内容が煩雑になってくるので、cmakeディレクトリを作り、その下に<ライブラリ名>.cmakeファイルを作成してそこに記述しています。
ルート直下のCMakeLists.txtには以下のようにして参照するように記述します。
# CMakeLists.txt
include(cmake/CLI11.cmake)
include(cmake/cpr.cmake)
インストールしたライブラリをリンクさせることも必要です。どこにどのライブラリが必要かを考える必要があるので、CMakeLists.txtの記述がそのままソフトウェア設計につながります。
target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11)
testディレクトリ
個人開発でテストの導入までしない人も多いのかもしれませんが、個人的にgoogletestの使い方をマスターしたかったので、googletestによるユニットテストの実装も試しています。
# tests/CMakeLists.txt
set(TEST_SOURCES
src/main.cpp
src/CurrencyTest.cpp
src/FormatterTest.cpp
src/FxTest.cpp
src/FxInterfaceTest.cpp)
add_executable(runTests ${TEST_SOURCES})
# add include directory
target_include_directories(runTests PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_include_directories(runTests PRIVATE include)
# make link to googletest
target_link_libraries(runTests PRIVATE libfx gtest gmock)
include(GoogleTest)
gtest_discover_tests(runTests)
基本的にはこれまでのCMakeLists.txtと同じで、必要なファイルやディレクトリを追加していきます。
ユニットテスト自体はrunTestsと命名して実行ファイルを生成し、これにlibfxをリンクさせることで、テストスクリプトからテスト対象のコードを呼び出せるようにしています。
# tests/CMakeLists.txt
add_executable(runTests ${TEST_SOURCES})
# add include directory
target_include_directories(runTests PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_include_directories(runTests PRIVATE include)
target_link_libraries()
の記載を以下のようにすることもできるのですが、これだとgmockによるMock関数が機能しません。Mockクラスを使う場合は、gmockをリンクするようにしましょう。
# make link to googletest
target_link_libraries(runTests PRIVATE libfx gtest gtest_main)
最後の2行を加えることによって、ctestコマンドでテストを実行できるようになります。
# tests/CMakeLists.txt
include(GoogleTest)
gtest_discover_tests(runTests)
これに加えて、ルート直下のCMakeLists.txtにてenable_testing()
を記載する必要があります。
CMakeLists.txtを各階層に持つ理由は、CMakeのための記述を分散させ、各ディレクトリごとに関心のある内容のみを記述するためと理解しています。そのため、このenable_testing()
もtests/CMakeLists.txt
に記載したかったのですが、これはルート直下のCMakeLists.txtでないとダメなようです。
自分の場合は、CMAKE_BUILD_TYPE
がReleaseの場合はユニットテストを除外したかったので、以下のようにしました。
# CMakeLists.txt
set(DEFAULT_BUILD_TYPE "Debug")
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "Including googletest and tests in non-Release build")
enable_testing()
include(cmake/gtest.cmake)
add_subdirectory(tests)
endif()
まとめ
今回、個人的にCMakeLists.txtの書き方を理解する上での要点を絞って書いてみました。細かいオプションは公式ドキュメントを参照してください。
自分も最初はCMakeは学習コストが高いように感じましたが、書いているうちに分かるようになりました。
しかし、web系のフレームワークならば、コマンド一つでプロジェクトのボイラープレートが作成できる時代。CMakeに関しても同様のものが欲しいところです。開発の過程でconanやvpckgといったパッケージマネージャーの存在を知ったので、次はそれらを試してみようと思います。
参考