GNU Makeでout-of-source build
out-of-source build とは
プロジェクトのビルドディレクトリをソースディレクトリと別に作成する手法。 中間ファイルや生成物が整理されるため、プロジェクトの管理が楽になる。
CMakeなどのビルドシステムを使えば簡単できるものではあるが、ここではGNU Makeを使ってout-of-sourceビルドを実現してみる。
環境
ここでは、C言語でnの階乗(n!)を計算するプログラムをプロジェクト例としたい。
プロジェクト構成
プロジェクト内には次のサブディレクトリがあり、同種ファイルがまとめられている。
- include:ヘッダファイル (.h)
- src:ソースファイル (.c)
proj ├ include │ └ fractional.h ├ src │ ├ fractional.c │ └ main.c └ Makefile
各ソース/ヘッダファイルの内容を次に示す。
include/fractional.h
#ifndef FRACTIONAL_H_ #define FRACTIONAL_H_ int fractional(int n); #endif
src/fractional.c
#include "fractional.h" // 階乗計算 int fractional(int n) { return (n <= 0) ? 1 : n * fractional(n - 1); }
src/main.c
#include <stdio.h> #include "fractional.h" // 10!の計算と結果の出力 int main(void) { printf("%d\n", fractional(10)); return 0; }
Makefile
上記ソースのビルド結果を別のディレクトリに出力し、さらにdebug/releaseビルドの結果をそれぞれ別のディレクトリに出力する用に設定してみよう。
Makefileを次に示す。
# ディレクトリの設定 INCDIR := include SRCDIR := src BUILDDIR := build # コンパイルオプション CC := gcc CFLAGS := -Wall -Wextra -Wpedantic -std=c11 -I$(INCDIR) # DEBUGマクロで最適化・デバッグ情報の有無を切り替える DEBUG ?= no ifeq ($(DEBUG), yes) CFLAGS += -O0 -g CONFIG := debug else CFLAGS += -O2 CONFIG := release endif TARGET := $(BUILDDIR)/$(CONFIG)/prog RM := rm -rf # ソースファイルと中間ファイル名を取得 SRCS := $(wildcard $(SRCDIR)/*.c) OBJS := $(addprefix $(BUILDDIR)/$(CONFIG)/,$(SRCS:.c=.o)) DEPS := $(OBJS:.o=.d) .PHONY: all clean all: $(TARGET) # ヘッダ依存関係のインクルード -include $(DEPS) # ビルドターゲットの生成 $(TARGET): $(OBJS) $(CC) $(CFLAGS) $^ -o $@ # オブジェクトファイルの生成 $(BUILDDIR)/$(CONFIG)/%.o: %.c @mkdir -p $(BUILDDIR)/$(CONFIG)/$(SRCDIR) $(CC) $(CFLAGS) -c -MMD -MP $< -o $@ clean: $(RM) $(BUILDDIR)
ポイントとしては次のようなものがある。
DEBUG
マクロの状態でdebug/releaseビルドを切り替える。ヘッダの依存関係をgccの
-MMD
オプションによりdependencyファイル (.d) に出力しインクルードする。これによりヘッダ内容の変更時にも再ビルドされる。ソースファイルは
wildcard
で検索し、ディレクトリ・拡張子に対する文字列処理で中間ファイル名 (.o/.d) を生成する。オブジェクトファイルの生成時に
mkdir -p
でディレクトリを作成する。
ビルド
Makefileと同ディレクトリでmake
するとreleaseビルドが実行され、build/release以下に実行ファイルprog
、build/release/src以下に中間ファイルが生成される。
$ ls Makefile include src $ make gcc -Wall -Wextra -Wpedantic -std=c11 -Iinclude -O2 -c -MMD -MP src/fractional.c -o build/release/src/fractional.o gcc -Wall -Wextra -Wpedantic -std=c11 -Iinclude -O2 -c -MMD -MP src/main.c -o build/release/src/main.o gcc -Wall -Wextra -Wpedantic -std=c11 -Iinclude -O2 build/release/src/fractional.o build/release/src/main.o -o build/release/prog $ ls -R build build: release build/release: prog src build/release/src: fractional.d fractional.o main.d main.o $ ./build/release/prog 3628800 # 10!
make DEBUG=yes
でdebugビルドが実行される。
$ make DEBUG=yes gcc -Wall -Wextra -Wpedantic -std=c11 -Iinclude -O0 -g -c -MMD -MP src/fractional.c -o build/debug/src/fractional.o gcc -Wall -Wextra -Wpedantic -std=c11 -Iinclude -O0 -g -c -MMD -MP src/main.c -o build/debug/src/main.o gcc -Wall -Wextra -Wpedantic -std=c11 -Iinclude -O0 -g build/debug/src/fractional.o build/debug/src/main.o -o build/debug/prog $ ls -R build/debug/ build/debug/: prog src build/debug/src: fractional.d fractional.o main.d main.o # デバッグ $ gdb ./build/debug/prog ... Reading symbols from ./build/debug/prog...done. # mainにブレークポイントを貼る (gdb) break main Breakpoint 1 at 0x679: file src/main.c, line 5. (gdb) run Starting program: ... Breakpoint 1, main () at src/main.c:5 5 printf("%d\n", fractional(10)); # デバッグ情報を付加しているため、対応するソース位置を確認できる (gdb) l 1 #include <stdio.h> 2 #include "fractional.h" 3 4 int main(void) { 5 printf("%d\n", fractional(10)); 6 7 return 0; 8 } 9 (gdb)
make clean
でbuildディレクトリを削除する。
$ make clean rm -rf build $ ls Makefile include src
問題・課題
wildcard
では再帰的な探索ができないため、include、srcディレクトリが階層化されていると対応できない。
対策としては、ディレクトリごとにMakefileを用意して(階層化)多段コンパイルする方法や、シェルコマンドを実行してファイルを検索する方法がある。