Makefileでワイルドカードを使う方法

はじめに

最近、C言語でコードを書くことが多く、頻繁にmakeコマンドを使っている。
ただ、単純な設定だと、ファイルを追加するごとにMakefileにもファイル名を追記しなければならず、この操作が大変煩わしい。
そこで、Makefileの勉強を兼ねて、これを自動化する設定を考えてみた。

要件

要件は、

  • 実装ファイル (.c) を追加した時に自動でコンパイルの対象になること。
  • ソースファイルがあるディレクトリ (src) とは別の、出力用ディレクトリ (build) に実行ファイルが生成されること。
  • buildディレクトリではsrcのディレクトリ構造が維持された状態で、中間オブジェクトファイルが生成されること。

とする。

サンプルのディレクトリ構成

コンパイル前のソースツリーは以下の通り。

.
├── Makefile
├── include
│   └── common.h
└── src
    ├── lib
    │   ├── error.c
    │   ├── inet_pton.c
    │   ├── readline.c
    │   ├── signal.c.bk
    │   ├── wrapsock.c
    │   └── writen.c
    └── tcpserv.c

インクルードファイルはincludeに、ソースファイルはsrc以下に格納されている。
src直下にはメインファイルであるtcpserv.cがあり、src/libにはライブラリファイルが格納されている。

これをmakeコマンドでコンパイルした時、以下のディレクトリ構成になることを目指す。

.
├── Makefile
├── build
│   ├── src
│   │   ├── lib
│   │   │   ├── error.o
│   │   │   ├── inet_pton.o
│   │   │   ├── readline.o
│   │   │   ├── wrapsock.o
│   │   │   └── writen.o
│   │   └── tcpserv.o
│   └── tcpserv
├── include
│   └── common.h
└── src
    ├── lib
    │   ├── error.c
    │   ├── inet_pton.c
    │   ├── readline.c
    │   ├── signal.c.bk
    │   ├── wrapsock.c
    │   └── writen.c
    └── tcpserv.c

成果物出力用のbuildが作成され、その中にコンパイルされたオブジェクトファイル、実行ファイルが作成される。
build内にはsrcおよびsrc/libが作成され、元のsrcと同じ構成が維持される。

Makefile

最終的なMakefileは次のようになった。

PROGNAME := tcpserv
INCDIR := include
SRCDIR := src
LIBDIR := lib
OUTDIR := build
TARGET := $(OUTDIR)/$(PROGNAME)
SRCS := $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/$(LIBDIR)/*.c)
OBJS := $(addprefix $(OUTDIR)/,$(patsubst %.c,%.o,$(SRCS)))
#$(warning $(OBJS))

CC = gcc
CFLAGS = -Wall -O2 -I $(INCDIR)

.PHONY: all clean
all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

$(OUTDIR)/%.o:%.c
	@if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi
	$(CC) $(CFLAGS) -o $@ -c $<

clean:
	rm -rf $(OUTDIR)

各所説明

定義
PROGNAME := tcpserv
INCDIR := include
SRCDIR := src
LIBDIR := lib
OUTDIR := build
TARGET := $(OUTDIR)/$(PROGNAME)
SRCS := $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/$(LIBDIR)/*.c)
OBJS := $(addprefix $(OUTDIR)/,$(patsubst %.c,%.o,$(SRCS)))

最初にファイルやディレクトリの定義を行っている。
代入演算子 ":=" は、コロンなしの "=" が必要時に代入されるのに対し、":=" は即時代入される。
通常は ":=" を使っておけば良さそうだ。

SRCSには必要なソースファイルを列挙して代入している。
ここで、wildcard関数を使うことによって、今回目的としている、src直下及びsrc/libに追加された ".c" ファイルを自動的にコンパイル対象としている。

OBJSには必要となるオブジェクトファイルを列挙して代入している。
必要なオブジェクトファイルとは、SRCSのソースファイルをコンパイルしてオブジェクト化した ".o" ファイルである。
そのため、patsubst関数を使って、SRCSのファイル名から ".c" を ".o" に置き換えることで定義ができる。
オブジェクトファイルが作成されるのは build ディレクトリ以下となるので、addprefix関数で $(OUTDIR) を接頭辞として連結している。

コメントアウトしているwarning関数は、makeコマンド実行時にログを出力するものである。Makefileデバッグ時には重宝する関数だ。

コンパイラの設定
CC = gcc
CFLAGS = -Wall -O2 -I $(INCDIR)

CCでは使用するコンパイラgccに設定している。
CLAGSはオプションの設定だ。
-I オプションを使用すると、ソースコード内で使用しているインクルードのディレクトリ設定ができる。

ダミーターゲットの設定
.PHONY: all clean
all: $(TARGET)

makeコマンドを引数なしで実行すると、最初に設定されているターゲットをコンパイルする。
今回はダミーターゲットとして all を設定しているため、今後ターゲットが増えた場合にも、all の依存ファイルを増やすことで対応ができる。
.PHONYを設定しているのは、カレントディレクトリに同名のファイルがあった場合にコンパイルできなくなることへの対策である。

実行ファイルのコンパイル
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

allをターゲットとしたコンパイルで、$(TARGET)が依存ファイルとして設定されているので、今度は $(TARGET) をターゲットとしたコンパイルが実行される。

ここでは "$" を使ったマクロを使用してコンパイル方法を指定している。
変数とマクロと展開すると以下のgcc実行コマンドとなる。

gcc -Wall -O2 -I include -o build/tcpserv build/src/tcpserv.o build/src/lib/error.o build/src/lib/inet_pton.o build/src/lib/readline.o build/src/lib/wrapsock.o build/src/lib/writen.o

なお、マクロは色々用意されているが、今回使ったマクロは以下3つである。

マクロ 説明
$@ ターゲット名
$^ 依存ファイルのリスト
$< 最初の依存ファイル
必要なオブジェクトファイルのコンパイル
$(OUTDIR)/%.o:%.c
	@if [ ! -e `dirname $@` ]; then mkdir -p `dirname $@`; fi
	$(CC) $(CFLAGS) -o $@ -c $<

$(TARGET)をターゲットとしたコンパイルで、$(OBJS)が依存ファイルとして設定されているので、今度はこの$(OBJS)に列挙されたオブジェクトファイルのコンパイルが実行される。

1行目の書き方が分かりづらいが、$(OUTDIR)%.o をターゲットとして、このファイルパスから$(OUTDIR)を取り除き、かつ拡張子を ".c" に変換したファイルを依存ファイルとしてコンパイルが実行されることになる。

2行目については、dirnameコマンドで各ターゲットファイル $@ のディレクトリ名を抽出し、このディレクトリの存在チェックを行った上で、なければ mkdir を実行している。

3行目は実際のコンパイルコマンドで、以下のコンパイルが実行される。

gcc -Wall -O2 -I include -o build/src/tcpserv.o -c src/tcpserv.c
gcc -Wall -O2 -I include -o build/src/lib/error.o -c src/lib/error.c
gcc -Wall -O2 -I include -o build/src/lib/inet_pton.o -c src/lib/inet_pton.c
gcc -Wall -O2 -I include -o build/src/lib/readline.o -c src/lib/readline.c
gcc -Wall -O2 -I include -o build/src/lib/wrapsock.o -c src/lib/wrapsock.c
gcc -Wall -O2 -I include -o build/src/lib/writen.o -c src/lib/writen.c

make cleanの設定
clean:
	rm -rf $(OUTDIR)

最後に make clean 実行時の内容を定義している。
今回成果物は全てbuildに生成されるので、このディレクトリごと消している。

まとめ

ひとまず要件は満たすことができた。
当初は色々戸惑ったものの、ターゲットのコンパイルに必要な依存ファイル、さらにその依存ファイルのコンパイルに必要な依存ファイルが順にコンパイルされていく、という仕組みをイメージすると、わかりやすい。
他にも便利な機能が用意されているので試していきたい。