気が向いたら書くやつ

気が向いたら何か書きます

GNU Makeでout-of-source build

簡単なC言語のプロジェクトをGNU Makeで管理する。

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を用意して(階層化)多段コンパイルする方法や、シェルコマンドを実行してファイルを検索する方法がある。

giraffydev.hatenablog.com

参考

www.wagavulin.jp

qiita.com