公開日: 2024年3月25日

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
~/cmake/myapp/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 に対する指示は、CMakeLists.txt というスクリプトで与えます。myapp.c と同じディレクトリに次の内容でファイルを作ります。

~/cmake/myapp/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(myapp)
add_executable(myapp myapp.c)

このようにたった 3 行でいいそうです。より詳しくは、公式ドキュメントを参照してください [2][3]

ビルドする

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 です。これらの図にしたがって各ファイルの中身をざっくりと見ていきます。

図 1
図 1
図 2
図 2

Makefile

プロジェクトをビルドするときに最初に読み込まれる 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 行目から、このファイルにやって来ます。

CMakeFiles/Makefile2 (抜粋)
...
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/dependCMakeFiles/myapp.dir/buildが実行されます。buildのほうがその名のとおり myapp をビルドするルールになっており、次の節で説明します。dependのほうは CMake が内部コマンドでなんかやっており、よくわからないので飛ばします (そんなんばっかりですみません)。

CMakeFiles/myapp.dir/build.make

このファイルに実行可能ファイル myapp をビルドするためのルールが書かれています。つまり、このファイルが myapp のビルドプロセスの本体といえます。

CMakeFiles/myapp.dir/build.make (抜粋)
...
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 のほうはコンパイル定数やインクルード・ディレクトリを設定するためのファイルのようです。

CMakeFiles/myapp.dir/flags.make
C_DEFINES =

C_INCLUDES =

C_FLAGS =

compiler_depend.ts のほうは中身が空だったので、用途がよくわかりません。

それから、実行可能ファイル myapp は link.txt というファイルにも依存しています。こちらも中身を確認してみると、リンクのコマンドラインらしいものが書かれていました。おそらくこのコマンドラインそのものか、それに近い形でリンクが行われるんだと思います。

CMakeFiles/myapp.dir/link.txt
/usr/bin/cc CMakeFiles/myapp.dir/myapp.c.o -o myapp

まとめ

CMake を使って、最小限のプロジェクトをビルドする Makefile を作成し、その中身を確認しました。ここからわかったことは

  • ビルドツリーのルートディレクトリには、Makefile と実行可能ファイルと CMakeFiles ディレクトリができる。
  • CMakeFiles の下には <ソースツリーのルートディレクトリ名>.dir ディレクトリができる。
  • その中には build.make があったり、コンパイルされたオブジェクトファイルが置かれたりする。
  • build.make にはビルドプロセスの本体が書かれている。

といった点です。次回は、プロジェクトにサブディレクトリとソースファイルを追加して、静的ライブラリとしてリンクしたら Makefile はどう変わるか?を試してみたいと思います。

参考資料

Raspberry Pi Pico 実験室