Make / CMake / Meson ビルドシステム

このページの目次

ビルドシステムが解決するのは、「この成果物を得るために、どの入力が先に準備されなければならず、どのような変更がどの手順の再実行を要求するか」である。コンパイル可能なコマンドを1つ書き出すのは始まりに過ぎず、インクリメンタルビルド、並列実行、依存関係の伝播が正しいかどうかは、グラフが完全かどうかにかかっている。

Make はルールとファイルの状態に基づいてタスクを実行する。CMake と Meson は通常、まず設定を行い、バックエンドのビルド記述を生成してから、Ninja や Make などの実行エンジンに渡す。CMake の設定フェーズの時間を Ninja のコンパイル時間と直接比較して、ある言語が本質的にビルドが遅いという結論を出してはならない。

同一の最小プロジェクト

以下の3つのアプローチは、同じファイル群をビルドする。ソースコードはプロジェクトのルートディレクトリに配置されている:

/* lib.h */
#ifndef DEMO_LIB_H
#define DEMO_LIB_H
int twice(int x);
#endif
/* lib.c */
#include "lib.h"
int twice(int x) { return x * 2; }
/* main.c */
#include "lib.h"
int main(void) { return twice(21) == 42 ? 0 : 1; }
インクリメンタルビルドの依存関係グラフ:共通ヘッダーファイルへの変更は2回のコンパイルに影響する lib.h main.c lib.c main.o lib.o libdemo.a prog main.o と lib.o は並列実行可能。リンクには main.o と更新された静的ライブラリの両方が完了している必要がある。

lib.c を変更した場合、ライブラリの再ビルドとリンクを行うべきであり、共通ヘッダーファイル lib.h を変更した場合、両方の使用側で再コンパイルを行うべきである。変更がない状態でビルドを実行した場合、不要なコンパイル動作があってはならない。このヘッダーファイルの依存関係を見逃すと、古い宣言と新しいライブラリが混在する可能性があるが、「全量ビルドが成功する」だけではこの問題は発見できない。

GNU Make:ルール、変数、自動依存関係

Makefile として保存する。以下のレシピ行の最初の文字は実際のタブ文字であり、通常のスペースに置き換えてはならない:

.DEFAULT_GOAL := all
CC ?= cc
AR ?= ar
CFLAGS ?= -O2 -g -Wall -Wextra
CPPFLAGS ?=
LDFLAGS ?=
LDLIBS ?=
OBJS := main.o lib.o

.PHONY: all clean
all: prog

prog: main.o libdemo.a
	$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)

libdemo.a: lib.o
	$(AR) rcs $@ $^

%.o: %.c Makefile
	$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -MF $(@:.o=.d) -MT $@ -c $< -o $@

-include $(OBJS:.o=.d)

clean:
	$(RM) $(OBJS) $(OBJS:.o=.d) libdemo.a prog

ここで $@ はターゲット、$< は最初の実行依存関係、$^ は重複を除いた実行依存関係のリストである。ライブラリ引数はそれを使用するオブジェクトの後に配置するのが、伝統的な静的ライブラリの抽出ルールにおける正しい順序である。

CPPFLAGS にはプリプロセッサ引数を、CFLAGS には C コンパイラ引数を、LDFLAGS にはリンカオプションを、LDLIBS には -lm などのライブラリ依存関係を配置する。分類することで、呼び出し元が1つのカテゴリのみを上書きでき、他のフェーズを誤って傷つけずに済む。

コンパイル時に各オブジェクトの .d ファイルを同時に生成する。-MMD は通常、システムヘッダーの依存関係を省略し、-MP はヘッダーファイルに対して補助的な空ルールを生成し、-include は最初に .d ファイルが存在しない場合でも許容する。Make は生成されたファイルを読み込んで、間接的な include 関係を把握する。GNU Make 自動実行依存関係

make -j4
./prog
make

Make は主にタイムスタンプと依存関係に基づいて更新を判断し、「コマンドライン引数が変わった」ことはデフォルトでは記録しない。本例では Makefile 自体をオブジェクトの依存関係として設定しているため、ファイルの編集で再コンパイルが行われるが、コマンドラインで CFLAGS を変更しても、古いオブジェクトは自動的に無効化されない。実際のプロジェクトでは、設定の署名、分離されたビルドディレクトリ、または設定切替時のクリーンアップを使用できる。

ヘッダーファイルの生成を明示的に依存関係として宣言する必要がある。最初に .d ファイルがない場合、自動スキャンだけでは生成コマンドが先に実行されることを保証できない。出力ディレクトリは通常、不要な再コンパイルをトリガーしないよう、順序のみ依存条件(order-only prerequisite)として適している。

CMake:ターゲットが使用要件を保持する

CMakeLists.txt として保存する:

cmake_minimum_required(VERSION 3.16)
project(demo VERSION 1.0 LANGUAGES C)

add_library(demo STATIC lib.c)
target_include_directories(demo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_features(demo PUBLIC c_std_11)

add_executable(prog main.c)
target_link_libraries(prog PRIVATE demo)
cmake -S . -B build-cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build-cmake --parallel 4
./build-cmake/prog

PUBLIC は、demo 自体と demo を使用するターゲットの両方がその属性を必要とすることを意味する。PRIVATE はターゲット自身のみを対象とし、INTERFACE は使用側にのみ伝達される。ヘッダーファイルが公開依存関係として特定の型、コンパイル定義、または言語特性を必要とする場合、対応する使用要件が伝播すべきかどうかを検討する必要がある。CMake ビルドシステム

静的ライブラリの「実装依存関係は公開しない」からといって、最終的なリンクでそれを必要としないわけではない。ビルドシステムはターゲットの種類に応じてリンク依存関係の伝播を処理する。ターゲット関係を使用し、include パス、ライブラリ名、グローバルフラグを複製してあちこちに配置してはならない。

CMAKE_BUILD_TYPE は、サンプルのシングル構成ジェネレーターに適している。マルチ構成ジェネレーターは通常、ビルド時に --config Release を通じて選択され、2つのモデルを混在させてはならない。コンパイルデータベースは実際のコンパイルコマンドを記録し、clangd などのツールに適している。LSP と言語サーバー を参照。

Meson:明示的なターゲットと依存オブジェクトの宣言

meson.build として保存する:

project('demo', 'c', version: '1.0',
        default_options: ['c_std=c11', 'warning_level=2'])

inc = include_directories('.')
lib = static_library('demo', 'lib.c', include_directories: inc)
demo_dep = declare_dependency(link_with: lib, include_directories: inc)
executable('prog', 'main.c', dependencies: demo_dep)
meson setup build-meson --buildtype=release
meson compile -C build-meson -j 4
./build-meson/prog

dependency オブジェクトを使用して、リンクライブラリと include 要件を一緒に渡すことで、消費者がそのうちの1つを見逃すことを防ぐ。最適化とデバッグの戦略は通常、buildtype またはオプションで表現され、各ターゲットの c_args に -O2 をハードコードする必要はない。特に、異なるコンパイラとの互換性を考慮する必要がある。Meson ターゲット、Meson 依存関係

一度 setup されたディレクトリは再設定(reconfigure)可能である。コンパイラの変更、クロス設定など、重要な環境を変更する際には、独立したビルドディレクトリを使用することで、キャッシュの混用を防ぐのに役立つ。設定が自動的にいくつかのフォールバック依存関係をダウンロードするかどうかは、プロジェクトとオプションによって決まり、dependency() を一律にパッケージマネージャーのインストールコマンドと理解してはならない。

生成ヘッダーファイルを追加し、完全なビルドグラフを検証する

factor.txt に整数 2 を書き込み、以下の generate.py を新規作成する。これは 1〜100 しか受け付けないようにし、任意の入力を直接 C ソースコードに連結しないようにする。出力はコマンドで指定された場所に配置する。

from pathlib import Path
import sys
source, output = map(Path, sys.argv[1:])
factor = int(source.read_text().strip())
if not 1 <= factor <= 100:
    raise SystemExit("factor must be in 1..100")
output.write_text(f"#define DEMO_FACTOR {factor}\n")

lib.c と main.c をそれぞれ以下の内容に置き換え、lib.h は前述の宣言を維持する。twice は元の関数名を維持するが、実際の倍数は設定によって制御される。例では 21 のみ呼び出されており、範囲が int のオーバーフローを引き起こすことはない。

/* lib.c */
#include "lib.h"
#include "generated.h"
int twice(int x) { return x * DEMO_FACTOR; }
/* main.c */
#include "lib.h"
#include "generated.h"
int main(void) { return twice(21) == 21 * DEMO_FACTOR ? 0 : 1; }
生成ヘッダーファイルはコンパイル入力であり、初回ビルドでも依存関係エッジが必要である factor.txt + generate.py データまたは生成器の変更 generated.h 最初に唯一の生成ルールによって生成される lib.c のコンパイル DEMO_FACTOR を読み取る main.c のコンパイル 同様に DEMO_FACTOR を読み取る コンパイラは .d をコンパイル時に生成する。存在しない .d に頼って、欠落しているヘッダーファイルを事前に生成することはできない。

Make で初回ビルドの依存関係を明示的に宣言する

前述の Makefile に以下のルールを追加し、clean レシピの削除リストに generated.h を追加する:

PYTHON ?= python3
main.o lib.o: generated.h

generated.h: factor.txt generate.py
	$(PYTHON) generate.py factor.txt $@

両方のオブジェクトは generated.h に依存する必要がある。最終的な prog のみに依存させると、並列ビルド中にヘッダーファイルがない状態でソースコードのコンパイルが先に始まってしまう可能性がある。ここで生成されるヘッダーファイルは通常の前置依存関係である。これを変更すると、使用側は再コンパイルされるべきである。誤って order-only に書き換えると、順序のみが保証され、内容の変化による再コンパイルの要件は表現されない。

最初の成功したコンパイル後、.d は実際の include 関係をさらに記録するが、初回ビルドの生成ルールには手動でのエッジ作成が必要である。複数の出力を生成する場合、それらが同じ正しいルールに属していることを確認し、複数のタスクが同時に同じファイルに書き込むことを避ける。

CMake で唯一の生成ターゲットを使用して複数の消費者を接続する

以下のスニペットを既存の CMakeLists.txt に追加し、demo と prog の両方が定義された後に配置する:

find_package(Python3 REQUIRED COMPONENTS Interpreter)
set(generated_header "${CMAKE_CURRENT_BINARY_DIR}/generated.h")
add_custom_command(
  OUTPUT "${generated_header}"
  COMMAND "${Python3_EXECUTABLE}"
          "${CMAKE_CURRENT_SOURCE_DIR}/generate.py"
          "${CMAKE_CURRENT_SOURCE_DIR}/factor.txt"
          "${generated_header}"
  DEPENDS generate.py factor.txt
  VERBATIM)
add_custom_target(generate_header DEPENDS "${generated_header}")
add_dependencies(demo generate_header)
add_dependencies(prog generate_header)
target_include_directories(demo PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")

カスタムコマンドはファイル出力を持ち、共通の生成ターゲットは両方の消費者が生成完了後にコンパイルされることを保証する。ヘッダーファイルの内容変更後の微細な粒度の再コンパイルは、コンパイル依存関係スキャンによって完了する。VERBATIM はパス引数を正しく渡すのを助け、手動でシェルのクォートを連結するのを避ける。生成器が必要な副生成物も生成する場合、適用可能なバージョンで BYPRODUCTS を使用して宣言する必要がある。Ninja が必要な入力をコマンド内部に隠してはならない。CMake カスタム出力

Meson で生成ターゲットを使用側のソースリストに配置する

このステップでは、元の meson.build を以下に置き換える:

project('demo', 'c', version: '1.0',
        default_options: ['c_std=c11', 'warning_level=2'])
python = import('python').find_installation('python3')
header = custom_target('generate_header',
  input: 'factor.txt', output: 'generated.h',
  command: [python, files('generate.py'), '@INPUT@', '@OUTPUT@'])
inc = include_directories('.')
lib = static_library('demo', ['lib.c', header], include_directories: inc)
demo_dep = declare_dependency(link_with: lib, include_directories: inc)
executable('prog', ['main.c', header], dependencies: demo_dep)

ここで include_directories('.') には対応するソースディレクトリとビルドディレクトリの両方が含まれ、消費者のソースリスト内の header は生成順序を表現する。generate.py はファイルオブジェクトとしてコマンドに参加し、データ factor.txt は明示的な入力であり、両方の変更がビルド判断に入る。生成ディレクトリを指すだけの include パスを書くだけでは、ビルドシステムが生産者を推測することを期待できない。Meson ソース生成

4種類の変更で3つのビルドアプローチを検証する

3つの実験は、それぞれ独立したソースコードのコピーを使用して行うことを推奨する。Make はソースディレクトリに generated.h を生成し、CMake/Meson はそれぞれのビルドディレクトリに生成する。同じソースディレクトリを混在させると、C の引用符による include が Make の残留ヘッダーファイルを優先して見つけ、別のビルドが誤った生成結果を使用する可能性がある。

変更期待される動作発見される問題
新規ディレクトリ、並列ビルドまず生成し、2つのオブジェクトをコンパイルし、リンクする初回の依存関係欠落
変更なしで再ビルド生成もコンパイルも行わない無条件の実行による無効な作業
factor.txt を 2 から 3 に変更再生成、使用側の再コンパイル、再リンク順序のみを表現し、変化を表現していない
generated.h を削除出力を自動的に再構築し、ビルドを成功させる出力が登録されていないか、依存関係キャッシュが問題を隠している

生成器は例では毎回実際に実行されるとタイムスタンプを更新するため、伝播を理解しやすい。もし「内容が変わらない場合は書き戻さない」方式を採用して後続のコンパイルを減らす場合、Make では入力が出力より新しい場合の処理も必要であり、毎回生成器を再実行しないようにする必要がある。プロジェクトの要件に応じてスタンプと実際の出力の関係設計を行い、Python の書き込みロジックを変更するだけでは、インクリメンタル動作が完全に完了したとはみなせない。

1本のエッジが足りない場合、どの成果物が静かに古くなるのか?

生成ヘッダーファイルの内容変更は、main.o と lib.o の両方を無効にする必要がある。main.o へのエッジを削除すると、リンクが別のパスによってトリガーされても、main.o は古いマクロ値を引き続き使用し、混合されたバージョンの成果物が形成される可能性がある。新規ビルドでは、生成がコンパイルより先に実行されることを保証するためにこれらのエッジが必要である。インクリメンタルの正しさと初回の並列実行の正しさは、それぞれ検証する必要がある。

図を準備しています
1本のエッジが足りない場合、どの成果物が静かに古くなるのか?

ビルドグラフは、変更された入力からすべての消費者へ伝達される必要がある。最終プログラムのみが生成ヘッダーファイルに依存させても、オブジェクトファイルのコンパイル依存関係を代替することはできない。

pkg-config:インストール済みライブラリのビルドメタデータの照会

pkg-config --modversion zlib
pkg-config --cflags --libs zlib
pkg-config --static --libs zlib

.pc ファイルは、ヘッダーファイルの場所、ライブラリ、および関連する依存関係について使用者に通知するが、ライブラリのインストールは行わず、ランタイムローダーがそれを見つけられることを保証するものでもない。パスはディストリビューション、アーキテクチャ、およびインストールプレフィックスに応じて変化するため、/usr/lib64/pkgconfig のように固定してはならない。

以下は demo.pc の内容の例であり、テキストファイルとして保存する必要がある。シェルスクリプトとして実行するものではない:

prefix=/usr/local
libdir=${prefix}/lib
includedir=${prefix}/include

Name: demo
Description: Small example library
Version: 1.0
Libs: -L${libdir} -ldemo
Cflags: -I${includedir}

公開依存関係と静的リンク時に必要なプライベート依存関係を区別し、状況に応じて Requires、Requires.private、Libs.private を記入する。--static 照会はより完全な依存関係情報を提供し、リンカにすべての静的ライブラリを選択させることを単独で強制するものではない。pkg-config 作者ガイド

CMake では、インポート済みターゲットを優先して消費し、${ZLIB_LIBRARIES} のみをコピーしてディレクトリやリンクオプションを見逃さないようにする:

find_package(PkgConfig REQUIRED)
pkg_check_modules(ZLIB REQUIRED IMPORTED_TARGET zlib)
target_link_libraries(prog PRIVATE PkgConfig::ZLIB)

このコードを前述の CMakeLists.txt に追加するだけで、prog に依存関係を追加できる。すでに適した find_package(ZLIB) インポートターゲットがある場合は、pkg-config を経由せずに直接使用することもできる。FindPkgConfig

クロスコンパイルと再現可能なビルド

ビルド時に実行されるコード生成器はビルドマシンのものであり、最終プログラムはターゲットマシンのものである。クロスコンパイルでは、コンパイラ、sysroot、ツール、およびターゲット依存関係の検索パスをそれぞれ指定する必要がある。ホストマシンの .pc 結果を誤用し、リンカーがアーキテクチャを自動的に修正してくれると期待してはならない。

CMake のツールチェインファイルと Meson のクロスファイルは、これらの環境を記述するために使用される。より完全な再現性は、依存関係のバージョン、生成器、環境変数、パスの埋め込み、および時間情報にも関与しており、ビルドシステムを使用しただけで自動的に得られるものではない。

比較と検証の方法

注目点検証すべき動作
正しさクリーンビルド、ヘッダーファイル変更、生成ファイル依存関係がすべて正しい
インクリメンタル効率変更がない場合は再コンパイルしない、部分的な変更は必要なターゲットのみを更新する
並列実行複数タスクのビルドで、偶発的なファイルの欠落や競合状態が発生しない
設定切替Debug、Release、異なるコンパイラ間で古い成果物が混在しない
統合コンパイルデータベース、テスト、インストール、パッケージメタデータがプロジェクトの要件に適合する

ビルドの速さは、主に実際のコンパイル作業、依存関係グラフの粒度、キャッシュ、および実行エンジンに依存する。Make にも依存関係グラフがあり、CMake も Ninja を生成できる。「命令型は遅く、宣言型は速い」という理由で測定を代替してはならない。

キーワード:Make, CMake, Meson, Ninja, 依存関係グラフ, depfile, ターゲット, 使用要件, pkg-config, クロスコンパイル, インクリメンタルビルド