CMake 覚え書き (1): 最小限のプロジェクトを作って Makefile を眺める
Raspberry Pi Pico のブートシーケンスを調べるために Raspberry Pi Pico SDK (以下 Pico SDK) のソースコードを読んでいるのですが、その前に CMake について知っておいたほうがよさそうな気がしてきました。CMake は Pico SDK をビルドするのに使われ、特にセカンダリ・ステージ・ブート・ローダの構築周りは CMake の機能を駆使したつくりになっているように見えます。そこで、ちょっと遠回りにはなりますが CMake について勉強し、わかったことを何回かにわたってまとめていきます。
今回は、まず最小限のプロジェクトを作って CMake を実行し、どのような Makefile が生成されるのかを確認してみます。
目次
CMake とは
ざっくりいうと、ソフトウェアの開発環境を自動的に判別し、それに適合するビルドシステムを生成してくれるソフトウェアです。例えば GNU 開発環境がインストールされているマシンでは、GCC でコンパイルして LD でリンクするような Makefile を自動的に生成してくれます。Windows の Visual Studio なんかにも対応しているようです。他にもさまざまな特徴があるようですが、ここではとても説明できないので、詳細は公式サイトを確認してください [1]。
今回試すプロジェクト
今回試すプロジェクトは、C 言語のソースファイルが 1 個だけ、関数も main 関数 1 個だけ、というものです。~/cmake/myapp ディレクトリを作って、その中にソースコードを置くことにします。
% cd ~/cmake/myapp
% tree
.
└── myapp.c
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("hello, world\n");
return 0;
}
それでは CMake を使って、この myapp.c から実行可能ファイルをビルドするための Makefile を生成してみましょう。
CMake のインストール
FreeBSD の場合は、ports/packages で簡単にインストールすることができます。
# pkg install cmake
インストールされた CMake のバージョンは 3.26.1 でした。
CMakeLists.txt を書く
ビルドする
CMake では、ソースコードを置くディレクトリと、ビルドの生成物が置かれるディレクトリを分けてビルドすること (Out-of-source ビルド) が推奨されているようです。そこで、上で作った myapp ディレクトリとは別に、ビルド用のディレクトリを作ります。
% mkdir ~/cmake/build/
次に CMake を実行します。-S
オプションでソースツリー (CMakeLists.txt を置いたディレクトリ) を、-B
オプションでビルドツリーを指定します。
% cd ~/cmake/
% cmake -S myapp -B build
この結果、build ディレクトリの下に Makefile や、ビルドに必要なその他のファイルが生成されます。あとはビルドを実行すれば、実行可能ファイルの出来上がりです。
% cmake --build build
% ./build/myapp
hello, world
生成された Makefile を眺める
ビルドが成功したあとの build ディレクトリの中を見てみます。
% tree build
build
├── cmake_install.cmake
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.26.1
│ │ ├── CMakeCCompiler.cmake
│ │ ├── CMakeCXXCompiler.cmake
│ │ ├── CMakeDetermineCompilerABI_C.bin
│ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ ├── CMakeSystem.cmake
│ │ ├── CompilerIdC
│ │ │ ├── a.out
│ │ │ ├── CMakeCCompilerId.c
│ │ │ └── tmp
│ │ └── CompilerIdCXX
│ │ ├── a.out
│ │ ├── CMakeCXXCompilerId.cpp
│ │ └── tmp
│ ├── cmake.check_cache
│ ├── CMakeConfigureLog.yaml
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeScratch
│ ├── Makefile.cmake
│ ├── Makefile2
│ ├── myapp.dir
│ │ ├── build.make
│ │ ├── cmake_clean.cmake
│ │ ├── compiler_depend.make
│ │ ├── compiler_depend.ts
│ │ ├── depend.make
│ │ ├── DependInfo.cmake
│ │ ├── flags.make
│ │ ├── link.txt
│ │ ├── myapp.c.o
│ │ ├── myapp.c.o.d
│ │ └── progress.make
│ ├── pkgRedirects
│ ├── progress.marks
│ └── TargetDirectories.txt
├── Makefile
└── myapp
ソースファイル 1 個だけのプロジェクトにしては、ずいぶんたくさんのファイルが作られました。ビルドツリーのルートには Makefile、実行可能ファイル、CMakeFiles ディレクトリなどが作成されています。CMakeFiles ディレクトリの下に myapp.dir というディレクトリがあり、この中身がソースツリーの構成によって変わるものと思われます。このうち本稿で注目するのは、Makefile、Makefile2 と myapp.dir ディレクトリの下にあるいくつかのファイルです。その他のファイルはよくわからないのでスルーします。
主要な部分だけを抜き出して、ターゲット間の依存関係を有向グラフ風に描いてみたのが図 1 です。また、デフォルトターゲット (単に「make」または「gmake」と打ったときに実行されるターゲット) だけに注目して、さらにすっきりさせたのが図 2 です。これらの図にしたがって各ファイルの中身をざっくりと見ていきます。
Makefile
プロジェクトをビルドするときに最初に読み込まれる Makefile です。
...
default_target: all
.PHONY : default_target
...
cmake_force:
.PHONY : cmake_force
...
CMAKE_COMMAND = /usr/local/bin/cmake
...
all: cmake_check_build_system
$(CMAKE_COMMAND) -E cmake_progress_start /home/mijinco/cmake/build/CMakeFiles /home/mijinco/cmake/build//CMakeFiles/progress.marks
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all
$(CMAKE_COMMAND) -E cmake_progress_start /home/mijinco/cmake/build/CMakeFiles 0
.PHONY : all
...
cmake_check_build_system:
$(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0
.PHONY : cmake_check_build_system
ターゲットを指定せずにmake
を実行すると、Makefile の先頭にあるターゲットから処理が始まります [4]。上の Makefile では、default_target
のルールから処理されるわけですが、依存関係によってdefault_target
の前にall
が、all
の前にcmake_check_build_system
が、というようにビルドが進みます。all
のレシピにあるように、このあとの処理は CMakeFiles/Makefile2 に移行します。
ところで、この Makefile を見ると、どうも CMake は Makefile を生成したらお役御免というわけにはいかず、実際のビルドプロセスにもいろいろ絡んでいるようです。cmake_progress_start
とか--check-build-system
とか、なんとなく何をやっているのか想像がつくネーミングではありますが、詳しいことはよくわかりません。
ちなみに「.PHONY
なんちゃら」と書かれているのは、そのターゲットと同名のファイルが存在した場合に、レシピが実行されない問題を回避するためのテクニックです [5]。また、「cmake_fouce
」という名前の、依存する前提条件もレシピも持たないターゲットがあります。このようなターゲットは、常に更新されたとみなされます。したがって、そのターゲットに依存するターゲットは、そのレシピが強制的に実行されることになります [6]。
CMakeFiles/Makefile2
最上位の Makefile の 12 行目から、このファイルにやって来ます。
...
all: CMakeFiles/myapp.dir/all
.PHONY : all
...
CMakeFiles/myapp.dir/all:
$(MAKE) $(MAKESILENT) -f CMakeFiles/myapp.dir/build.make CMakeFiles/myapp.dir/depend
$(MAKE) $(MAKESILENT) -f CMakeFiles/myapp.dir/build.make CMakeFiles/myapp.dir/build
@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --progress-dir=/home/mijinco/cmake/build/CMakeFiles --progress-num=1,2 "Built target myapp"
.PHONY : CMakeFiles/myapp.dir/all
...
CMakeFiles/myapp.dir/all
のレシピでは、CMakeFiles/myapp.dir/build.makeにある 2 つのルール、CMakeFiles/myapp.dir/depend
とCMakeFiles/myapp.dir/build
が実行されます。build
のほうがその名のとおり myapp をビルドするルールになっており、次の節で説明します。depend
のほうは CMake が内部コマンドでなんかやっており、よくわからないので飛ばします (そんなんばっかりですみません)。
CMakeFiles/myapp.dir/build.make
このファイルに実行可能ファイル myapp をビルドするためのルールが書かれています。つまり、このファイルが myapp のビルドプロセスの本体といえます。
...
CMakeFiles/myapp.dir/myapp.c.o: CMakeFiles/myapp.dir/flags.make
CMakeFiles/myapp.dir/myapp.c.o: /home/mijinco/cmake/myapp/myapp.c
CMakeFiles/myapp.dir/myapp.c.o: CMakeFiles/myapp.dir/compiler_depend.ts
@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green --progress-dir=/home/mijinco/cmake/build/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building C object CMakeFiles/myapp.dir/myapp.c.o"
/usr/bin/cc $(C_DEFINES) $(C_INCLUDES) $(C_FLAGS) -MD -MT CMakeFiles/myapp.dir/myapp.c.o -MF CMakeFiles/myapp.dir/myapp.c.o.d -o CMakeFiles/myapp.dir/myapp.c.o -c /home/mijinco/cmake/myapp/myapp.c
...
myapp: CMakeFiles/myapp.dir/myapp.c.o
myapp: CMakeFiles/myapp.dir/build.make
myapp: CMakeFiles/myapp.dir/link.txt
@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green --bold --progress-dir=/home/mijinco/cmake/build/CMakeFiles --progress-num=$(CMAKE_PROGRESS_2) "Linking C executable myapp"
$(CMAKE_COMMAND) -E cmake_link_script CMakeFiles/myapp.dir/link.txt --verbose=$(VERBOSE)
...
CMakeFiles/myapp.dir/build: myapp
.PHONY : CMakeFiles/myapp.dir/build
...
CMakeFiles/myapp.dir/depend:
cd /home/mijinco/cmake/build && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /home/mijinco/cmake/myapp /home/mijinco/cmake/myapp /home/mijinco/cmake/build /home/mijinco/cmake/build /home/mijinco/cmake/build/CMakeFiles/myapp.dir/DependInfo.cmake --color=$(COLOR)
.PHONY : CMakeFiles/myapp.dir/depend
CMakeFiles/myapp.dir/build
が実行されると、myapp.c をコンパイルして myapp.c.o を生成し、それをリンクして実行可能ファイル myapp を生成する、というつくりになっています。myapp.c.o は、ソースコードの他に flags.make と compiler_depend.ts というファイルにも依存しています。中身を確認してみると、flags.make のほうはコンパイル定数やインクルード・ディレクトリを設定するためのファイルのようです。
C_DEFINES =
C_INCLUDES =
C_FLAGS =
compiler_depend.ts のほうは中身が空だったので、用途がよくわかりません。
それから、実行可能ファイル myapp は link.txt というファイルにも依存しています。こちらも中身を確認してみると、リンクのコマンドラインらしいものが書かれていました。おそらくこのコマンドラインそのものか、それに近い形でリンクが行われるんだと思います。
/usr/bin/cc CMakeFiles/myapp.dir/myapp.c.o -o myapp
まとめ
CMake を使って、最小限のプロジェクトをビルドする Makefile を作成し、その中身を確認しました。ここからわかったことは
- ビルドツリーのルートディレクトリには、Makefile と実行可能ファイルと CMakeFiles ディレクトリができる。
- CMakeFiles の下には <ソースツリーのルートディレクトリ名>.dir ディレクトリができる。
- その中には build.make があったり、コンパイルされたオブジェクトファイルが置かれたりする。
- build.make にはビルドプロセスの本体が書かれている。
といった点です。次回は、プロジェクトにサブディレクトリとソースファイルを追加して、静的ライブラリとしてリンクしたら Makefile はどう変わるか?を試してみたいと思います。
参考資料
[1] | CMake 公式サイト https://cmake.org/ |
[2] | CMake リファレンス・マニュアル https://cmake.org/cmake/help/latest/index.html#reference-manuals |
[3] | CMake チュートリアル https://cmake.org/cmake/help/latest/guide/tutorial/index.html |
[4] | How make Processes a Makefile https://www.gnu.org/software/make/manual/html_node/How-Make-Works.html |
[5] | Phony Targets https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html |
[6] | Rules without Recipes or Prerequisites https://www.gnu.org/software/make/manual/html_node/Force-Targets.html |