ごく普通なGNU Make活用例

2012年半ばぐらいから拡張機能ありでGNU Makeを使用するようになったが、makeの基本的なところを通り越したあたりの話題――実践の場にて各機能をどんな風に使用するかについての情報がなかなか見つからない。特に拡張機能の話題が少ない。

仕方なくNetBSD Make使いの師匠筋の人のMakefileの書き方を参考にしているのだが、世の中的にはどう評価されるのか気になる。ちゃんとGNU Makeっぽいやり方になっているのだろうか?

あまりにも気になるので、ちょっとした例題を考えて、それに対する私の解法をさらしてみることにする。

お題:C言語で書かれた簡単なアプリをビルドする

こんな感じの普通のコンソールアプリ。

/* ********************************************************************** */
/**
 * @file    args.c
 * @brief   コマンドの引数を1行1引数で表示する(コマンド名は表示しない)
 *
 * @bug  改行を含む引数のことを考慮していない。
 */
/* ********************************************************************** */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	int i;

	for (i = 1; i < argc; i++) {
		(void) puts(argv[i]);
	}

	return EXIT_SUCCESS;
}

わざわざMakefileを書くほどでもない? いや、ビルド時にどんなオプションを指定したか記録を残しておきたくて……メモを残す代わりにMakefileにしておこうかと。

MinGW + Cygwinな環境でビルドできるようにする

普段の作業環境はWindows上に構築してある。Cygwinをインストール済みで、コンパイラ回りのみTDM-GCCMinGW-w64/TDM64)に差し替えてある。

まずは、この環境で args.c をビルドして実行ファイルを生成するMakefileを書いてみる。ビルドによる生成物を削除する擬似ターゲット clean も追加しておく。

# @file    Makefile
# @brief   args Makefile for GCC (MinGW + Cygwin).

app        := args

CC         := gcc
CFLAGS     := -Wall -ansi -pedantic -finput-charset=cp932

.PHONY: all
all: $(app)

.PHONY: clean
clean:
	$(RM) $(app)

変数CCに明示的にgccを設定している。CygwingccではなくTDM-GCCを使用している関係で、私の環境にはccが存在しないからだ。

CFLAGSの -finput-charset=cp932 はおそらく不要だが、ソースコードをCP932で記述しているので、念のため付けてある。

このMakefileは(Makefile文字コードと改行コードに注意しておけば)Cygwinだけでなくgccを使えるUnix環境(例えばUbuntu)でも問題なく使用できる。

Mac OS Xでもビルドできるようにする

今度はMac OS Xでもビルドできるようにする。正確には、gccではなくclangを使用し、かつUniversal Binaryにしたい。

ここで、先ほどのMakefileをコピーして書き換えるのではなく、共通化を図りたい。

1つのMakefileを使用してターゲット名に応じて差分を変更する、という方法に今まで何度か挑戦したものの、ターゲット固有変数の書き換えではうまい具合に実現できないことが多かった(やり方が間違っていた可能性もあるが)。

最近は共通部分を記述したファイルとビルド環境固有の部分を記述したファイルに分割するようにしている。

まずは共通部分のファイル。

# @file    Makefile_common
# @brief   args Makefile (common for Unix like environment).

app        := args
CFLAGS     += -Wall -ansi -pedantic

.PHONY: all
all: $(app)

.PHONY: clean
clean:
	$(RM) $(app)

アプリ名やCFLAGSに設定する最低限のオプション、各ターゲットの記述はCygwinLinuxMac OS Xも同じだ。

次に、最初に書いたMakefileと同等の設定でビルドするための差分ファイル。

# @file    Makefile_gcc
# @brief   args Makefile for GCC (Unix like environment).

CC         := gcc
CFLAGS      = -finput-charset=cp932

include ./Makefile_common

CCにgccを設定し、CFLAGSに -finput-charset=cp932 が追加されるようにした上で、先ほどの共通ファイルをインクルードしている。実際にビルドする際は「make -f Makefile_gcc」という風にこのファイルを明示的に指定する。

こちらはMac OS X用の差分ファイル。Xcode 6.1以降 + Command Line Toolsをインストール済みの環境を想定している。

# @file    Makefile_mac
# @brief   args Makefile for Mac OS X.

SDKROOT        := $(shell xcodebuild -version -sdk macosx10.10 | sed -n '/^Path: /s///p')
CPPFLAGS       := -isysroot "$(SDKROOT)"
TARGET_ARCH    := -mmacosx-version-min=10.8 -arch i386 -arch x86_64

include ./Makefile_common

単純なC言語のツールのビルドにまで必要なのか分からないが、使用するSDKのバージョンとサポートするOSの最小バージョンをオプションで明示している。-arch でi386x86_64を指定しているので、Intel Macの32ビットと64ビットの双方に対応したUniversal Binaryが生成される。

MinGW用の特別対応版をつくる

さて、ここにきて重要なことに気づいた。手元のTDM-GCCMinGW-w64ベースであるため、Makefile_gccでビルドすると64ビットWindows用の実行ファイルになってしまう。使用しているPCには32ビットWindowsの環境もあるので、32ビット用の実行ファイルもビルドできるようにしたい。

そこで、Makefile_gccをコピーして微修正した特別対応版の差分ファイルを作成することにする。

# @file    Makefile_mingw64_32bit
# @brief   args Makefile for MinGW-w64/TDM64 (build 32bit binary).

CC         := gcc
CFLAGS      = -finput-charset=cp932 -m32

include ./Makefile_common

CFLAGSに -m32 を付け足しただけ。

自動テストの仕組みを追加する

ここまでで、以下の環境でビルドするためのMakefile一式ができあがった。

今度は、せっかくなので簡単な自動テストの仕組みを追加したい。実行時の引数を1行1引数で単純に表示するだけなので、テストデータを引数に指定してアプリを実行し、出力をテキストファイルに書き出し、期待される出力内容を記述したファイルとのdiffをとるようにする。

ディレクトリ構成はこんな感じ。

. (current directory)
|   Makefile_common
|   Makefile_gcc
|   Makefile_mac
|   Makefile_mingw64_32bit
|   args.c
|
\---test
    +---input
    |       01.txt
    |       02.txt
    |       03.txt
    |
    \---required
            01.txt
            02.txt
            03.txt

./test/input にはテストデータのテキストファイルを、./test/required には期待される結果のテキストファイルを配置する。実際の実行結果は ./test/output に書き出すようにする。テストごとに共通のファイル名を使用する。

テストは複数の環境で実施したいので、Makefile_commonに処理を記述する。

# @file    Makefile_common
# @brief   args Makefile (common for Unix like environment).

app        := args
CFLAGS     += -Wall -ansi -pedantic

.PHONY: all
all: $(app)

.PHONY: test
test: all
	@cd test;   \
	[ -d output ] || mkdir output;  \
	for i in input/*; do    \
		fn=`basename $$i`;  \
		echo test: $$fn;    \
		../$(app) `cat $$i` >output/$$fn;   \
		diff -u --strip-trailing-cr required/$$fn output/$$fn;  \
	done

.PHONY: clean
clean:
	$(RM) $(app)

.PHONY: distclean
distclean: clean
	$(RM) -r test/output

テストデータ ./test/input/01.txt は、例えばこんな感じ。

foo bar baz

期待される出力 ./test/required/01.txt はこんな感じ。

foo
bar
baz

実行結果は ./test/output/01.txt に出力される。./test/required/01.txt とのdiffをとる際、OSによる改行コードの違いを吸収するために --strip-trailing-cr を追加している。

この自動テストの仕組みは、少なくともCygwinMac OS XUbuntuで使用できる。

まとめ

最終的に、複数のUnixライクな環境でアプリをビルド && 自動テストするMakefile一式ができあがった。https://github.com/eel3/args/tree/master/cにほぼ同等のファイル一式(アプリ名を変えてコメントを英語にしたもの)を置いてある。

この例題は、実際にGNU Makeを使用したときの経験を下敷きにしている。実験用のコンソールアプリをWindowsMac OS XLinuxでビルドできるように構築したり、ライブラリのテスト用にちょっとしたシミュレータ*1を書いた上でビルドからテスト実行までを自動化したりするのにGNU Makeを使っているのだ。

今回は1つのソースファイルから直接コンソールアプリをビルドしているが、現実には複数のコンポーネントを使用することが多い。OSによってインクルードするヘッダファイルやビルドするソースコードを切り替えることもある。MinGWでは対応できない場合には、諦めてWindows版をVisual Studioでビルドするようにしている。

GNU Makeはそこそこ多くのOSで利用できる。小規模なプロジェクトや限定的な移植性が求められる程度のケースでは、力技を含めてそれなりに何とかなってしまうものだ。

ソースツリーが大規模だったり広範囲な移植性が求められる場合にはAutotoolsも選択肢に入ると思うが、不勉強なのでよく分からない。何よりAutotoolsで作成されたパッケージを使う側として、例えばUbuntuのLTSでAutotoolsのバージョンが古くてビルドできなかったとか、諸事情によりXcode 3.2.6からバージョンアップできなかったMac OS X環境でAutotoolsのバージョンが古くてビルドできなかったとか、WindowsCygwinやMSYS)でビルドできなかったとか、未だに2勝しかしていない人間なのでAutotoolsに対して不信感がある。ここ10年で2勝である。お祓いしてもらった方がよいのだろうか?

*1:テストデータを入力として、処理結果を出力するテキストフィルタ。パーサの類のテストで重宝している。