工学系にとってのCmakeの壁
私は学部4年生でロボット系研究室に配属された時、Cmakeの壁にぶち当たりました。
情報系の専攻であれば授業で習ったのかもしれません。
しかし、制御工学系コースで育ち、授業で軽くgccコンパイルは習ったものの基本はPythonで育ってきた私にとって、Cmakeは難しすぎました。
(専門用語の嵐、公式ドキュメントは英語、その引数はどこで定義されてたんだ、etc…)
本稿は、学部3~4年生にも分かるよう日本一優しい導入でいながら、自力でCMakeLists.txtが書けるようになる所まで行きます。
Cmakeにより、今まで
$ gcc my_code.cpp -I head1.h head2.h …
とか長々と書いていたコマンドが省略でき、全て自動で終わります。
環境はUbuntu18.04ですが、それ以外でもLinuxであれば大差ないと思います。
(Windows・macOSでもCmakeは動くらしいが、本稿では扱わない)
目次
- テンプレートの提供
- テンプレートのビルド
- Cmakeの基本ルール
- テンプレート全行解説
- 頻出関数チートシート
- 頻出引数チートシート
- (おまけ) 共有ライブラリ(.so)とは
(本稿は説明のために厳密性を省略している部分があります。
本稿を最初の1段として、他の記事を2段、3段と登ればCmakeへの理解はずっと深まるでしょう。)
1. テンプレートの提供
結論から言います。
このzipファイルをダウンロード・解凍して好き放題改造すれば、それだけで基本的には何でも動きます。
OpenCVでもPCLでも、あなたのプログラム内で自由に#include <***.h>
で呼び出せるようになります。
中身は簡単なC++のコード3枚と、CMakeLists.txtから成ります。
cmake_training
|- include/cmake_training
| my_class.h
|- src
| my_main.cpp
| my_class.cpp
|- CMakeLists.txt
そして、CMakeLists.txtはこうなってます。
CMAKE_MINIMUM_REQUIRED(VERSION 3.14)
project(cmake_training CXX)
message("project name is automaically set as: ${PROJECT_NAME}")
set(PROJECT_VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(Boost REQUIRED
COMPONENTS
context
filesystem
program_options
regex
system
thread
)
if(Boost_FOUND)
message (STATUS "Found Boost")
else(Boost_FOUND)
message(WARNING "Boost not found")
endif()
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
message (STATUS "Found OpenCV")
else(OpenCV_FOUND)
message(WARNING "OpenCV not found")
endif()
set(cmake_training_SRC
src/my_class.cpp
src/my_main.cpp
)
set(cmake_training_HEADERS
include/cmake_training/my_class.h
)
add_executable(main ${cmake_training_SRC} ${cmake_training_HEADERS})
target_include_directories(main PUBLIC include/cmake_training)
MESSAGE(STATUS "--------------------------------------------")
MESSAGE(STATUS "Info :")
MESSAGE(STATUS " Project Name = ${PROJECT_NAME}")
MESSAGE(STATUS " Version = ${PROJECT_VERSION}")
MESSAGE(STATUS " CMAKE_VERSION = ${CMAKE_VERSION}")
MESSAGE(STATUS " CMAKE_INSTALL_PREFIX = ${CMAKE_INSTALL_PREFIX}")
MESSAGE(STATUS " CMAKE_BUILD_TYPE = ${CMAKE_BUILD_TYPE}")
MESSAGE(STATUS " CMAKE_INSTALL_LIBDIR = ${CMAKE_INSTALL_LIBDIR}")
message(STATUS " PCL_VERSION = ${PCL_VERSION}")
message(STATUS " OpenCV_VERSION = ${OpenCV_VERSION}")
吐き気のするやつですね、はい。
3章からは、このテンプレートの内容を解説しながら、どうやって改造するかを説明していきます。
先に2章でout-of-sourceビルドの練習をしますが、分かる人は2章は流し読みで大丈夫です。飛ばして下さい。
2. テンプレートのビルド
ここではout-of-sourceと呼ばれるビルド形式を説明します。
gitで誰かのC++プログラムをcloneしてきた時は、基本的には以下の手順が正しいコンパイル手順となります。
gccで原始的にビルドしていくと".o"などのゴミファイルが散乱し、gitで管理する時に何をアップロードし何を無視するのか、スーパーカオスな状態に陥ります。あなたの研究開発は詰みです。
そこで今回のout-of-sourceビルド方式を使えば、ソースコード空間は一切汚れず、git管理も簡単になってハッピーという事です。
行きましょう。
各自ダウンロードしたzipファイルを解凍しておいて下さい。
まず、解凍したファイルに入ります。
$ cd cmake_training
次に、"build"ディレクトリを作成して下さい。
$ mkdir build
そしたら、buildディレクトリに入って…
$ cd build
cmakeを__【buildディレクトリの中で】__起動します。
$ cmake ..
cmakeの後には半角スペースが入り、その後にピリオドを2つです。
このピリオドは相対パスを示しており、1つ上のディレクトリを意味します。
必ずCMakeLists.txtのある場所を指して下さい。
そうすると、ずらずらっとターミナルに文字が表示されます。
最後に__Configring done__と__Generateing done__が表示されれば成功です。
これで準備が整ったので、コンパイルをします。
$ make
無事に100%まで達成すれば、コンパイル完了です。
作成したプログラムを実際に起動してみましょう。
$ ./main
無事に"start main function"と"Hello!(from my class)"が表示されれば成功です。おめでとう。
これを使えば、例えばgitにあるオープンソースを使いたい時も、
1.git clone https://github.com/***/***
2.buildディレクトリ作成 & 移動
3.cmake、からの make
で大体解決します。
この流れは基本中の基本なので覚えましょう。
3. Cmakeの基本ルール
ルール1:プロセスの流れ
CmakeにはCMakeLists.txtというファイルが必要です。(List s なので注意)
CMakeLists.txtに書いてあるコマンドを __上から順番に__実行します。Pythonみたいですね。
エラーが発生した瞬間に動作が停止するので、原因の特定は意外と簡単です。
ルール2:変数
Cmakeの変数は、基本的に__全て文字列__です。これを必ず念頭に置いて下さい。
色々な関数を駆使し、変数に文字列を代入していくことでコンパイルの設定を細かく決定するプロセスこそがCmakeです。
また、リスト構造といって、1つの変数に複数の文字列を格納することが可能です。
C言語の配列みたいな感じです。
自分が扱っている変数が単一の文字列なのか、それともリストなのか、非常に紛らわしいです。
また、Cmakeの関数は複数の引数を一気に入力できるものが多く、複数の要素をリスト変数に格納して一括で渡すことが多々あります。
これがCmakeを複雑怪奇たらしめる根源のひとつです。
ルール3:大文字 小文字
関数名は大文字小文字の区別はありませんが、変数には大文字小文字の区別があります。
例えばfind_package()
とFIND_PACKAGE()
という関数は完全に同じです。
しかし、find_package(OpenCV)
とfind_package(opencv)
では全く挙動が変わります。後者だと動きません。
くれぐれもお気をつけて。
ルール4:${〇〇〇}は単なる変数呼び出し
変数の参照(呼び出し)は ${〇〇〇} で行います。
あくまで変数の中身を読む時のみで、定義の時は必要ありません。
CMakeListsの中で嫌というほど目にする${}ですが、全て変数の中身を読んでいるだけです。怖くありません。
例えば、先ほどのテンプレートの末尾に沢山ある${PROJECT_NAME}
や${CMAKE_VERSION}
は
全て単純に変数の中身を参照しているだけです。
裏を返せば、全て事前に定義済みの文字列であったということですね。
(変数の定義についてはset()
という関数を使います。詳細は5章で。)
4. テンプレートの全行解説
では、さっそく配布テンプレートのCMakeLists.txtの説明に入ります。
1行目 : CMAKE_MINIMUM_REQUIRED(VERSION ○○○)
冒頭には必ず、CMAKE_MINIMUM_REQUIRED(VERSION ○○○) を書きます。
Cmakeソフトウェアの最低保証バージョンを明記し、設定します。
これよりも古いCmakeを使うと処理が停止します。アップデートしましょう。
2021/4/12現在、最新は3.20ですが、誰でも使いやすいようにテンプレートでは3.14にしておきました。
3.11と3.14で大きめのアップデートが入ったらしいので、基本的には3.14以上に設定しましょう。
2行目 : project(〇〇〇 CXX)
project関数を使ってプロジェクトの名前を決定しましょう。
テンプレートではcmake_trainingとしました。名前は自由です。
実は、この関数は自動的にいくつか変数を設定します。
project()関数では、PROJECT_NAME
とPROJECT_SOURSE_DIR
の2つが宣言・定義されます。
内部的に、勝手に定義します。(諸悪の根源)
Cmakeはユーザーが認知しない所で勝手に変数を定義してくる極めて凶悪な自動システムなので、
初心者は「え、その変数いつ定義されたん!?」「多分こんな変数名があったはず…」という初見殺しに会います。
大丈夫です、6章に一覧を用意しました。
また、この関数は後半にもう1つ引数を取ります。
プログラム言語選択です。
今回はC++を使うので、CXX
と追記しましょう。
(CXXとはC++の意味です。+を45°回転させたヤツ誰だよ許さんからなマジで)
CMakeはC++以外にもC, CUDA, OBJC, OBJCXX...など色々な言語をサポートしています。
(僕は基本C++以外使いませんが。)
3行目 : message("~~~")
いわゆるprintfです。
2章で貼ったスクリーンショットにも見えますが、cmakeコマンド実行時にターミナルに文字を表示します。
今回のテンプレートでは文字の中に${PROJECT_NAME}
を仕込んでいます。
画像をよく見ると"project name is automatically set as : cmake_training"
とありますね。
そう、つまりこの時点で、変数PROJECT_NAME
が既に自動定義されているわけです。
4行目 : set(PROJECT_VERSION 〇.〇.〇)
set()関数ですね。
使い方は、set(A B)のように2つ引数を与えて呼び出します。
Aという変数を宣言し、AにBを代入します。
(もし変数Aが既に存在する場合は、Aの中身をBで上書きします。)
ここではPROJECT_VERSIONに〇.〇.〇という数字(厳密には文字列)を代入しています。
テンプレートでは 1.0.0 としました。
開発が進むにつれて 2.13.0 みたいに プログラム管理者の裁量で 数値が進みますが、あまり気にせず大丈夫です。
(一応、この数値は.soファイルの出力時に使われます。詳細は7章)
#####5行目~7行目 : C++17の有効化
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_EXTENSIONS OFF)
全て先ほどと同じset関数です。
CMAKE_CXX_STANDARDには17を代入することで、C++17でのコンパイルを指定しています。
C++20が使いたい時は20を指定して下さい。
後ろの2行はおまじないみたいなものです。
C言語の授業で、とりあえず1行目に#include <stdio.h>
を書かされたのと同じと思って下さい。
(詳細は ModernCmakeやCmake公式を読むと正確)
8行目 ~ 16行目: find_package(〇〇〇 (version) REQUIRED)
遂にfind_packageです。
見覚えのある人も多いのではないでしょうか。
この関数を使うと、Cmakeが自分のPCの中からライブラリを探してくれます。
(今回のテンプレートでは一般的なBoostライブラリを探してみました)
そして発見した場合には、複数の変数を自動定義します。
例えば、Boost_FOUND
やBOOST_INCLUDE_DIRS
, BOOST_LIBARIES
などです。
その証拠に、直後18行目のif(Boost_FOUND)が正しく働き、
分岐内のmessage関数を起動させていることが実行画面で確認できます。
試しにmessage("${BOOST_INCLUDE_DIRS}")
をCMakeLists.txtに追記して実行してみましょう。
上手く動いていれば/usr/include等が出るはずです。
ただし、find_package()関数がやってくれることは__変数の定義__までです。
この変数(ヘッダーのパスなど)を、gccコンパイルで言うところの -I や -L のオプション引数に渡すような作業が必要になります。
REQUIREDを付けると、もしライブラリが発見できなかった場合には直ちに処理が停止します。
複雑なCMakeListsになると、〇〇というライブラリが無ければ代わりに△△を使う等、find_packageで発見できなかったからといって
処理を中断するとは限らない芸当が可能になるわけです。
初心者は大人しくREQUIREDを付けいましょう。
また、REQUIREDの前に1.8などのversionを挿入すれば、その特定のバージョンだけを探索します。(省略可能)
特にOpenCVやPCLはバージョンごとに関数の仕様が大きく変化し、挙動が変化するため、バージョン指定すると大変安定して動作します。
更に、テンプレートの中にはその後ろに
COMPONENTS 〇〇 〇〇 〇〇...
といった小文字の羅列がありますね。
実はBoostやOpenCV等のメジャーなライブラリは、その巨大さから複数のコンポーネントに分割されています。
Boostにはcontextやfilesystemといった小さなコンポーネントが含まれているわけです。
必要な機能がBoostのごく一部の時には、そこだけ決め打ちで指名して連れ出す事で、あなたのプログラムの依存関係が少しコンパクトになります。
(コンポーネント名に何があるかはライブラリ次第なので、使う時に各自ググって下さい。)
とりあえず初心者はfind_package(〇〇 REQUIRED)
が書ければ良し。
(ちなみにROSでおなじみのcatkinも、COMPOENTSの後にtfやstd_msg等のROSパッケージ名を沢山羅列しますよね!まさにこれです。)
17行目 ~ 21行目 : if() ~ else()
プログラミング言語と同じif分岐です。else()の引数は空っぽでも可。endif()は必須
今回の括弧にはBoost_FOUNDが与えられています。
この変数は直前のfind_package()関数で自動的に裏で定義され、無事にライブラリを発見した場合にはTrueが代入されます。
Boost_FOUND
は変数なので、大文字・小文字の区別があります。
大文字のBOOST_FOUND
では機能しないのでお気をつけて。
この大文字・小文字の命名規則としては、パッケージ名と統一されている模様。
find_package(Boost)なら、そのままBoost_FOUNDでだいたい行ける.
22行目 ~ 27行目 : find_package (2回目)
OpenCVのインポート例です。
説明はBoostのものをご参照下さい。
ただし、テンプレートでは最初コメントアウトしてあります。
(煩雑なバグで初心者のモチベーションを2章から折ることを避けるため)
28行目 ~ 31行目 : set()によるソースコード一括定義
set(cmake_training_SRC src/my_class.cpp src/my_main.cpp)
既出のset()関数ですが、今回は引数が沢山あります。
こちらが実は、ルール2で軽く言及したリスト型変数の定義です。
今回はcmake_training_SRC
という1変数にソースコードを2枚渡しています。(相対パスです)
実際の大きなプロジェクトでは10枚ほど一括して纏めることも普通です。
とにかく引数の数が可変で、結構なオプション引数が省略OKという点がCmakeのトリッキーな点です。
どこで引数の種類が代わっているのか、落ち着いて見極めましょう。
32行目 ~ 34行目 : setによるヘッダファイルの一括定義
set(cmake_training_HEADERS include/cmake_training/my_class.h)
先のソースコード一括定義と同様です。
変数に一括してまとめています。
先ほどのsrcと共に、今回はユーザーが定義する変数のため、変数名は自由です。
35行目 : add_executable(target src header)
CMakeLists.txtで最も中心的な関数です。
また、親戚として add_library() という関数もあります。
とにかく add_executable() と add_library() がCmakeの世界の中心です。
誰かの書いたCMakeLists.txtを読む時は、最優先でadd_executable か add_libraryを探して下さい。
add_executableは実行ファイル名を宣言し、それを構成するソースとヘッダを引数として与えます。
今回のテンプレートではmainという名前で実行ファイルを宣言し、ソースコードにはつい先ほど自分で定義したcmake_training_SRC
と cmake_training_HEADERS
を与えています。
(変数の中身を参照ということで、${ }
で囲んでいることに注意です。)
ここでmainという名前を与えたため、コンパイル後の実行では./main
となります。
add_executable()やadd_library()の第一引数は、一般的に ターゲット と呼ばれます。
要はコンパイルの中心です。
__ターゲット__は中級ステップで非常に重要なワードになるので、絶対に覚えてください。
また一方で、add_library()はライブラリを作成します。
例えばOpenCVやPCL、Boostなどのライブラリは、実行コマンドが無い、言わば関数のセットですよね。
あれを構成するのがadd_libraryです。
開発者もユーザーもCmakeを統一して使えば、#include<〇〇.h>で簡単に関数を使用できます。
つまりCmakeを極めれば、世界中の天才たちのコードを自分の手元で動かし、開発に利用することができるのです。
少々話が逸れましたが、add_executable()とadd_library()はCmakeの中心です。
※2022/1/4 訂正:
ごめんなさい、厳密にはadd_executable()やadd_library()の引数にヘッダーファイルは不要でした。
(別にあっても困らないしバグったりはしませんが、一般的に.cや.cppファイルのみ与えるのが主流です。第二引数は空欄のままスキップでOKです。)
基本的にヘッダーファイルは次の target_include_directoriesで指定します。
target_include_directriesのパス下にヘッダーファイルを置くだけで、#include<***.h>呼び出しに応じて自動的に探索・発見してくれます。
そっちのほうがカッコよくキレイに書けます。推奨です。
36行目 : target_include_directories(target PUBLIC パスA パスB)
コンパイラにincludeのパスを通します。
gccで言うところの -I オプションでしょうか。
第1引数のtargetには、必ず先ほどのadd_executable(またはadd_library)で設定したターゲットを与えましょう。
そのプログラムのコンパイルに対象を絞って、パスを通します。
第2引数のPUBLICは、とりあえず入門者は気にしなくで良いです。(怒られそう)
その後ろに、与えたいincludeパスを相対パスで示します。
このテンプレートであれば、CMakeLists.txtとの位置関係から、include/cmake_trainingを指定します。
また、この関数も引数を無数に取れるタイプでして、複数のincludeパスを一括して与えることが可能です。
Boostライブラリを使用する場合には${BOOST_INCLUDE_DIRS}
を、
Eigen3なら ${EIGEN3_INCLUDE_DIRS}
を追加で与えましょう。
(末尾がDIR S と複数形になっている点に注目です。実はこれもリスト型変数で、複数のパスを一括保持してます)
くどいようですが、これらの変数はfind_package()関数が裏で定義しています。
必ず事前にfind_packageを使ってライブラリを探しましょう
37行目 : target_link_libraries(target PUBLIC 〇〇 〇〇)
コンパイラにライブラリ(.soファイル等)を渡します。
gccで言うところの -L オプションです。
第1引数のtargetには、必ず先ほどのadd_executable(またはadd_library)で設定したターゲット名を与えましょう。
第2引数のPUBLICは、先ほど同様、とりあえず入門者は気にしなくで良いです。
その後ろに、使用したいライブラリをターゲット名で渡します。
この関数も引数を無数に取れるタイプでして、複数のライブラリを一括して与えることが可能です。
Boostライブラリを使用する場合には${BOOST_LIBRARIES}
を、
Eigen3なら ${EIGEN3_LIBRARIES}
を追加で与えましょう。
もちろん、これらの変数もfind_package()関数による事前定義が必要になります。
どんな名前の変数で定義されているかはfind_packageに依りますので、明示的に探すことは不可能です。
命名規則も開発者によって曖昧で、予測しづらいです。
変数なので大文字・小文字の区別があり、運では中々当たりません。
「Eigen cmake」「OpenCV cmake」などでググれば分かります。
38行目 ~ 終わり
デバッグメッセージです。
message(STATUS 文章)
とすると出力がちょっとカッコよくなります!
お疲れ様でした!!全行コード解説は以上です!!
5. 頻出関数チートシート
-
set()
変数を手動で定義する。C言語で言うところの
set(USE_OPENCV TRUE)
のように、set(変数名 中身)で使う。
C言語的に表現すると、bool useOpenCV = true;
みたいな使用感。 -
find_package()
重要。ライブラリ名を与えると、関連するCMake変数を一括で設定してくれる超便利スーパーマン。
この関数さえ覚えて使えば、C++のコンパイルが急に簡単になる。
CMakeを使う最大のメリット。 -
add_executable()
重要。 .cppファイルを実行可能ファイル(.oとか)に変換する、まさしくコンパイル実行コマンド。cmakeの本質。
平たく言えばgcc -o MyFirstProgram first.cpp
に相当する。(※雑表現です)
add_executable(MyFirstProgram first.cpp second.cpp third.cpp)
という感じで、第1引数に完成物の名前(=MyFirstProgram)を書いたあと、2番目以降は材料となる.cppファイルを(順不同で)無限に羅列できる。 -
add_library()
上記の「add_executable()」の親戚。
.cppファイルをライブラリファイル(.soとか)に変換する、まさしくコンパイル実行コマンド。
コードをライブラリにすれば、#include <****.h>
みたいな使い方で他のコードから関数を呼出しできる。
CMakeLists.txtを読む上で絶対に見落としてはいけない。 -
target_include_directories()
準備中 -
target_link_libraries()
準備中 -
target_compile_definitions() (※中級者向き。2023/1/26追記)
.cppファイルや.hファイルが#ifdef
を使う時に対応するコマンド。CMakeLists.txtに
target_compile_definitions(USE_OLD_OPENCV)
と書いておくと、
変数「USE_OLD_OPENCV」が定義される。
これを使えば、.hや.cppの冒頭でよく見かける
#ifdef USE_OLD_OPENCV
#include <opencv2/hogehoge.h>
#else
#include <opencv3/new_hoge.h>
#endif
のようなマクロ分岐でTRUEの方に分岐する。
つまり、CMakeLists.txt内でif()関数と組み合わせれば、ライブラリのバージョンによってヘッダファイルを可変にできる。可能性の獣。真のニュータイプにしか扱えない。
-
add_subdirectory() (※中級者向き。2023/1/26追記)
CMakeLists.txtを入れ子構造にできる。整理整頓。 -
message()
いわゆるprintf()関数。CMakeLists.txt内に書いて使う。
cmakeコマンドを打つとき、ターミナル画面にメッセージを表示できる。 -
if()
6. 頻出変数チートシート
-
${PROJECT_SOURCE_DIR}
プロジェクトの根本のパスが文字列形式で自動格納される変数。
CMakeLists.txtにmessage(${PROJECT_SOURCE_DIR})
と追記すると、ターミナルにprintfデバッグっぽく表示できるので、変数の中身が分からなくなったらmessage()関数でデバッグしてみると良い。 -
${CMAKE_CURRENT_SOURCE_DIR}
準備中 -
${TARGETS_EXPORT_NAME}
じゅんび -
${PROJECT_VERSION}
実はこのPROJECT_VERSION
という変数は事前予約されている特殊な変数。
この変数を定義し、数値を与えると、Cmakeシステムが自動的にプロジェクトのバージョンであると解釈してくれる。
(そんなん誰が気付くねんみたいな隠し変数ですが、これがCmakeです。初見殺しです。)
7. 共有ライブラリとは ~ apt install 対 ソースビルド ~
apt であれば、完成済みの共有ライブラリファイル(.so)とヘッダファイル(.h)が /usr/libと/usr/include へダウンロードされる。
非常に楽な一方で、既に機械語であるため書き換えができない。
コンパイルの工程も挟まないため、例えばOpenCVではGPUオプションが切られていたりと、詳細な設定ができない。
自分でコード改造や再設定したければ、ソースコードからコンパイルする必要がある。
その際にadd_libraryがcmakeの中心となっている。
cmakeとmakeにより.soファイルが生成され、make install を行うと /usr/local/libへ.soファイルが、/usr/local/includeへ.hファイルがコピーされる。
この時、.soファイルにはバージョン情報(${PROJECT_VERSION})が追記され、.so.1.4のように明記される。
プロジェクトバージョンを書き換えることで複数の.soファイルを管理可能。
仕様に合わせて使い分けできる。