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-GCC(MinGW-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を設定している。Cygwinのgccではなく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に設定する最低限のオプション、各ターゲットの記述はCygwinもLinuxもMac 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 でi386とx86_64を指定しているので、Intel Macの32ビットと64ビットの双方に対応したUniversal Binaryが生成される。
MinGW用の特別対応版をつくる
さて、ここにきて重要なことに気づいた。手元のTDM-GCCはMinGW-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一式ができあがった。
- Mac OS Xでclangを使用してUniversal Binaryでビルドするもの。
- MinGW-w64/TDM64で32ビットWindows向けにビルドするもの。
- 上記以外の、Cygwinを含むgccを利用可能なUnix環境でビルドするもの。
今度は、せっかくなので簡単な自動テストの仕組みを追加したい。実行時の引数を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 を追加している。
まとめ
最終的に、複数のUnixライクな環境でアプリをビルド && 自動テストするMakefile一式ができあがった。https://github.com/eel3/args/tree/master/cにほぼ同等のファイル一式(アプリ名を変えてコメントを英語にしたもの)を置いてある。
この例題は、実際にGNU Makeを使用したときの経験を下敷きにしている。実験用のコンソールアプリをWindows/Mac OS X/Linuxでビルドできるように構築したり、ライブラリのテスト用にちょっとしたシミュレータ*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のバージョンが古くてビルドできなかったとか、Windows(CygwinやMSYS)でビルドできなかったとか、未だに2勝しかしていない人間なのでAutotoolsに対して不信感がある。ここ10年で2勝である。お祓いしてもらった方がよいのだろうか?
*1:テストデータを入力として、処理結果を出力するテキストフィルタ。パーサの類のテストで重宝している。