ソースコード、実行形式ファイル、中間ファイルを分けるmakefileを書く

この記事は C 言語のソースコードと実行形式ファイル、中間ファイルを分けてコンパイルする makefile を書いてみた記録です。（C++ にはちょっとした変更くらいで応用できそうです。）
なお、できあがったものは[このリポジトリ](https://github.com/MusicScience37/Test_make_20190428)に置いておきました。大したものではありませんので、私に責任を問わないという条件のもとでは自由に使っていただいて構いません。（執筆時点の最新コミット: bd5cd8fb79ee2f3837da5356169a72f1977770ff）
また、記事中に makefile のコードの一部を載せていますが、makefile はタブとスペースで意味が変わるため記事中のものをコピペしても正しく動かない可能性があります。

# 環境

- Ubuntu 18.04
- GNU Make 4.1
- gcc (Ubuntu 7.4.0-1ubuntu1~18.04) 7.4.0

# 前提

ここでは、

~~~~{tree.txt}
.
├── bin（実行ファイル）
│   ├── main1
│   └── main2
├── src（ソースコード）
│   ├── Makefile
│   ├── main1.c
│   ├── main2.c
│   ├── make_module_c.mk
│   └── test（main1.c, main2.cで使うプログラム）
│       ├── test.c
│       └── test.h
└── temp（コンパイル時にしか要らない中間ファイルを置くフォルダ）
~~~~

のようなフォルダ構造のもとで

- main1.c, test.c からなる main1
- main2.c, test.c からなる main2

という 2 つの実行形式を作るという状況を仮定しました。ただし、もっと大きめのプロジェクトに使えるようにするため、makefile は簡単に実行形式ファイルの数を増やせる書き方をするものとします。（ちょうどこれくらいの能力を持った makefile が欲しかったんです。）

なお、ソースコードの中身は次の通りです。

~~~~{test.h}
#pragma once

void test(int i);
~~~~

~~~~{test.c}
#include <stdio.h>

#include "test.h"

void test(int i) {
    printf("Test %d\n", i);
}
~~~~

~~~~{main1.c}
#include "test/test.h"

int main() {
    test(1);
    return 0;
}
~~~~

~~~~{main2.c}
#include "test/test.h"

int main() {
    test(2);
    return 0;
}
~~~~

単純に main1 を実行すると `Test 1` と表示され、 main2 を実行すると `Test 2` と表示されるというものです。番号を表示することで正しくリンクされているかを確認できるようにしました。

# Makefile の中身

今回 make に使うファイルは Makefile ファイルと make_module_c.mk ファイルに分けています。
Makefile ファイルは次のようにただコンパイルの条件を記述するのみです。

~~~~{Makefile}
CFLAGS := -Wall

include make_module_c.mk

$(eval $(call one-exe-rule, \
    main1, \
        main1.c \
        test/test.c))

$(eval $(call one-exe-rule, \
    main2, \
        main2.c \
        test/test.c))
~~~~

# make_module_c.mk の中身

make_module_c.mk が今回頑張った部分になります。
全容は [github](https://github.com/MusicScience37/Test_make_20190428) に置いてありますので、ここでは上から順にみていきます。

まず、基本的な変数定義をします。
QUIET 変数はコマンドの前に付けることでコマンドをシェル上に表示しなくするためのものです。VERBOSE 変数が定義されているとき（`make VERBOSE=1` で make を実行したときとか）には QUIET 変数を定義せず、コマンドをシェル上に表示するようになります。

~~~~{make_module_c_1.mk}
# suffix
OBJ_SUFFIX ?= .o
EXE_SUFFIX ?=

# commands
TEST ?= test
MKDIR ?= mkdir -p

# output directories
TEMP_DIR ?= ../temp
BIN_DIR ?= ../bin

# if VERBOSE variable is set, show commands
ifndef VERBOSE
    QUIET := @
endif
~~~~

次にデフォルトのターゲットを all にするための記述がきます。
これを他のターゲットの前に置くことで単に `make` と実行しても `make all` を実行したのと同じように動きます。

~~~~{make_module_c_2.mk}
# Default target is all
.PHONY: all
all:
~~~~

続いてファイルのパスを変化させるヘルパー関数をいくつか定義します。
これを利用してソースコードのパスから中間ファイルのパスを作ります。

~~~~{make_module_c_3.mk}
# $(call source-dir-to-temp-dir, directory-list)
source-dir-to-temp-dir = $(addprefix $(TEMP_DIR)/,$1)

# $(call source-to-object, source-file-list)
source-to-object = $(call source-dir-to-temp-dir, \
    $(subst .c,$(OBJ_SUFFIX),$(filter %.c,$1)))

# $(call source-to-depend, source-file-list)
source-to-depend = $(call source-dir-to-temp-dir, \
    $(subst .c,.d,$(filter %.c,$1)))
~~~~

次は bin フォルダや temp フォルダを作るための関数です。
これを呼び出してフォルダを作るようにすれば、必要なフォルダをあらかじめ `mkdir` で作っておく必要がなくなりますし、`make clean` は単に bin, temp フォルダを削除するだけにできます。

~~~~{make_module_c_4.mk}
# $(eval $(call prepare-directories, directory-list))
# prepare output directories (if not cleaning)
define prepare-directories
    ifneq ($(MAKECMDGOALS),clean)
        $(foreach f, $1, \
            $(eval TEMP_PREPARE_DIRECTORY := $(shell $(TEST) -d $f || $(MKDIR) $f)))
    endif
endef
~~~~

次は 1 つのソースコードをコンパイルするための関数です。
既にコンパイルのルールを作る処理をしたオブジェクトファイルについては PROC_OBJECT 変数に入れて置き、そこにない場合だけコンパイルのルールを作る処理をします。こうしないと同じファイルで 2 回この関数が呼ばれるときに warning が表示されて面倒でした。
また、ヘッダの依存関係をファイル（DEPEND 変数にパスを入れています）に保存しておき、それを利用してヘッダの変更に対して makefile が反応できるようにしています。（make-depend と呼ばれるテクニックです。）

~~~~{make_module_c_5.mk}
# variable to store processed object files
PROC_OBJECTS=

# $(call one-compile-rule-c, object-file, source-file)
# make the compile rule for a source file written in C
define one-compile-rule-c
    # avoid duplication
    ifeq (,$(findstring $1,$(PROC_OBJECTS)))
        $(eval DEPEND := $(call source-to-depend, $2))

        $1: $2
	        @echo "- compile $$<"
	        $(QUIET) $(CC) $(CFLAGS) -M $$< -MP -MT $$@ -MF $(DEPEND)
	        $(QUIET) $(CC) $(CFLAGS) -c $$< -o $$@

        -include $(DEPEND)

        PROC_OBJECTS+=$1
    endif
endef
~~~~

次にこの関数を使ってリストで渡されたすべてのソースコードのコンパイルのルールを作る関数を作りました。

~~~~{make_module_c_6.mk}
# $(call compile-rules, sources)
# make compile rules
define compile-rules
    $(foreach f, $(filter %.c, $1), \
        $(call one-compile-rule-c,$(call source-to-object,$f),$f))
endef
~~~~

そして、ソースコードとターゲットとなる実行形式ファイルの名前を受け取ってコンパイルのルールとリンクのルールの作成、フォルダの準備をする関数を書きました。一応 Windows の Mingw とかでも使えるように実行形式ファイルの拡張子を付ける仕様にしています。ただ、なぜこんなに eval を使わないといけないのかは私もよく知りませんので、なぜこうしないと動かないのかわかる方は教えてください。

~~~~{make_module_c_7.mk}
# variable to store all targets to clean all of them
PROC_TARGETS=

# $(call one-exe-rule, target, sources)
# make build rules for a executable
define one-exe-rule
    $(eval TARGET := $(addsuffix $(EXE_SUFFIX), $(addprefix $(BIN_DIR)/, $1)))

    all: $(TARGET)

    $(TARGET): $(call source-to-object, $2)
	    @echo "- link to build $(TARGET)"
	    $(QUIET) $(CC) $(LIBFLAGS) $$^ -o $$@

    $(eval $(call prepare-directories, $(call source-dir-to-temp-dir, $(dir $2))))
    $(eval $(call prepare-directories, $(addprefix $(BIN_DIR)/, $(dir $1))))

    $(eval $(call compile-rules, $2))

    PROC_TARGETS += $(TARGET)

endef
~~~~

最後に `make clean` のやり方を書いて出来上がりです。

~~~~{make_module_c_8.mk}
# clean target
.PHONY: clean
clean:
	@echo "- remove output directories"
	$(QUIET) $(RM) -r $(TEMP_DIR) $(BIN_DIR)
~~~~

# 参考文献

GNU Make 第3版, Robert Mecklenburg, O'REILLY. （菊池 彰 訳）