はじめに
MakeやCMakeは様々なプロジェクトで使われている,c/c++
コードのコンパイルを手助けしてくれる基盤技術です.
でも初心者には取っつきにくいですよね.チュートリアル通りにやったらなんか上手くいったけど,何やってるかよく分かんない!だから自分なりに変更できないし,エラーが出ても対処できない…ってことありませんか?(私はこれまでずっとそうでした…)
個人的にプロジェクトを立ち上げるにあたりMakeやCMakeについて初めてちゃんと知ったので,備忘録をかねてまとめておきます.同じ状況の初心者の助けになればと思います.
Makeとは?なぜ使うの?
ソースファイルが1つだけの時,そのコンパイルはごく簡単です.
g++ main.cpp -o main
これでmain
という実行ファイルが生成されます.
では次のようにもっとたくさんのファイルがある時にはどうなるでしょう?
- ソースファイル
main.cpp
file1.cpp
file2.cpp
- ヘッダファイル
file1.hpp
file2.hpp
この場合,コンパイルのコマンドは次のようになります.
g++ -c main.cpp -o main.o
g++ -c file1.cpp -o file1.o
g++ -c file2.cpp -o file2.o
g++ main.o file1.o file2.o -o main
ちょっと面倒ですねー…ファイルが増えたりコンパイラオプションを追加したりすることになったら,もっと面倒です.
そこで活躍するのがMakeです.上記のファイル一式をMakeでコンパイルにはどうすればいいのか見てみましょう.MakeではまずMakefile
いう名前のファイルを用意し,その中に次のように記載します.
SRC = main.cpp file1.cpp file2.cpp # source files
OBJ = $(SRC:.cpp=.o) # object files
CXX = g++ # compiler
CXXFLAGS = -Wall -std=c++11 # compile options (flags)
main: $(OBJ)
$(CXX) $(CXXFLAGS) -o main $(OBJ) # command to generate exe file
# How the object files are related
main.o: main.cpp file1.hpp file2.hpp
file1.o: file1.cpp file1.hpp
file2.o: file2.cpp file2.hpp
# clean
clean:
rm -f $(OBJ) main
ここで中身を理解する必要はありませんが,大体の感じは見て分かると思います.設定を記述すれば,Makeがコンパイルのための複雑なコマンドを生成してくれるのです.
Makefile
が作成できたら,次のコマンドを打ちます.
make
これだけです.ずいぶん楽になりましたね.
Makeを使うことの利点は次のようにまとめられます.
- 複雑なコンパイルコマンドを入力する必要がない
- ファイルが追加された場合は
Makefile
に追記すればいい - コンパイルオプションを変更する場合は
Makefile
を変更すればいい
加えて,Makeには変更が加えられたファイルのみを再コンパイルしてくれます.従ってたくさんのファイルがある場合には普通にコンパイルするよりも速く済むという大きな利点もあります.
ちなみに,このようにコンパイルの手助けをしてくれる仕組みのことを(特にCMakeの文脈で)ビルドシステムと呼びます.CMakeのほかに,MSBuild(Microsoft Build engine)やNinjaもビルドシステムの一種です.
CMakeとは?なぜ使うの?
これまでMakeを使うとコンパイルが楽になることを見てきましたが,Makeで十分コンパイルが楽になるならなぜCMakeも必要なのでしょうか?
実はMakeのみを使ったプロジェクトには次のような懸念事項があるんです.
- 世の中には他にもビルドシステムがあり,それぞれに適した設定ファイル(Makeでいう
Makefile
)が必要です.提供するプロジェクトの他のユーザーがMakeではなくMSBuildやNinjaを使いたい場合には,それぞれに適した設定ファイルを用意する必要があります. - ビルドシステムの設定ファイルは複雑で,独特な記法を採用していることが多いです.上記の
Makefile
の例も初見じゃよく分かんないですよね.
特に最初の点はチーム開発の際に問題となります.だってファイル追加するだけで全部のビルドシステム用の設定ファイルを書き換えるわけにいかないですよね.
CMakeはこういった問題を解決してくれます.CMakeの機能を端的にいえば「ユーザーが指定したビルドシステムが使う設定ファイルを用意する」ことです.例えばユーザーがMakeを使いたいならCMakeはMakefile
を生成してくれるということですね.
CMakeを使うには,CMakeLists.txt
という設定ファイルを用意します.その中身は先ほどのプロジェクトの例では次のようになります.
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable(main main.cpp file1.cpp file2.cpp)
さっきのMakefile
に比べてめっちゃ簡単になってますよね!
CMakeLists.txt
が準備できたら,次のコマンドを打ちます.
cmake -S . -B build
引数-S
はpath-to-source
を意味しており,CMakeLists.txt
を含むソースファイル一式が含まれたディレクトリへのパスを指定します.-B
はpath-to-build
を意味しており,ビルドシステム用の設定ファイルが生成される場所を指定します.上記のコマンドを打てば,build/Makefiles
が生成されるわけです.
ちなみに,よく次のように紹介されることもありますが,これも同じことをしています.
mkdir build
cd build
cmake ..
この場合だと一度build
ディレクトリを生成し,そこに移動してから,1階層上のCMakeLists.txt
を参照しています.先述の方法の方が端的で良いと思うのですが,CMakeのバージョンが古い場合には-S
や-G
を使うことができず,このやり方しかできないみたいです.
どのビルドシステムを使うかは,引数-G
で指定します.-G
はビルドシステムへの設定ファイルを生成する役割を持つCMake Generator
を意味しています.例えば,
cmake -S . -B build -G "Unix Makefiles"
はMakeが使うMakefile
を生成しますし,
cmake -S . -B build -G "Visual Studio 17 2022"
はMSBuildが使うVisual Studio 2022のプロジェクトファイル一式を生成しますし,
cmake -S . -B build -G "Ninja"
はNinjaが使うbuild.ninja
ファイルを生成します.ご自身の環境でどのビルドシステムが使えて,-G
に何を渡せばいいかは,cmake --help
で確認できます.
引数-G
は任意です.-G
が渡されなかった場合,CMakeはプラットフォームの状態に応じて決められたデフォルトのビルドシステムを採用します.現在のデフォルトはcmake --help
で確認できます.
CMakeを導入すれば,
- 設定ファイル(
CMakeLists.txt
)の記述が楽になる - プロジェクトをプラットフォームに依存しない形にできる
ということがお分かり頂けたのではないでしょうか.CMakeLists.txt
ファイルさえ作ってしまえば,ユーザーがどのOS,どのビルドシステムを使っていたとしても対応できちゃうんです!
ワークフロー
では最後にCMakeとMakeを使ったコンパイルの手順を確認しておきましょう.最終的なコマンドは次のようになります.
cmake -S . -B build -G "Unix Makefiles" # build/Makefileを生成
cd build # buildディレクトリへ移動
make # 生成されたMakefileを使って実行ファイルを生成
もっと簡単に,こんな風にコンパイルすることもできます.
cmake -S . -B build -G "Unix Makefiles"
cmake --build build
この場合Makeは呼び出しませんでしたが,cmake --build build
によってCMakeが自動的にMakeを呼び出し,実行ファイルの生成までやってくれます.
参考