最近社内でビルドの手順がちょいちょいMakefileで書かれているため、意外と知らなそうなmake及びMakefileの基本的な使い方をまとめてみました。
参考図書はこちらです https://www.oreilly.co.jp/books/4873112699/ 。
基本
まずは基本から。makeの本来の用途は、ソースコードのビルド手順を自動化することで、Makefileはその手順を記載したファイルになります。主にC言語をターゲットにしています。
一番基本的な内容はターゲットと依存、そしてビルド手順の3つを記載したルールで構成されています。たとえば、次のように書きます。
hello: hello.c
gcc hello.c -o hello
makeコマンドに引数としてターゲット名を指定することで、Makefile内のそのターゲットの手順を実行します。 ソースコードビルドなどの場合は、ターゲットは生成物の名前になると自然ですね。
$ make hello
gcc hello.c -o hello
また、差分ビルド機能もあって、 targetが更新されていない場合はその手順をスキップしてくれます。
$ make hello
make: `hello' is up to date.
Phony Targets
本来の使い方はビルドツールなのですが、最近はタスクランナーとしての使われ方が多いかもしれません。先程、ターゲットが生成物になるのが自然と言いましたが、ターゲットに生成物を伴わない書き方もできます。このようなターゲットをPhony Targetsと言います。
clean:
rm -f *.o lexer.c
ここでの注意点としては、"ファイルが存在してしまったら、差分なしと解釈されて実行されない”ということ。これを避けたい場合は、特殊な .PHONYというターゲットを使います。
.PHONY: clean
あえて.PHONYを使わないちょっとしたテクニックもあります。実行コマンドであえてファイルを作成しておくことで、ファイルが更新されてない時は再度実行されないようにできます。次の例はファイルが更新されてない時はサイズを測りなおさない例になります。
prog: size prog.o
$(CC) $(LDFLAGS) -o $@ $^
size: prog.o
size $^
touch size
変数
Makefile中に変数も記載できます。以下2通りの書き方どちらも有効です。
$(variable-name)
${variable-name}
変数への代入は少し気をつける必要があります。後ろのスペースは除去されません。コメントつけるときとかやりがちなエラーです。
LIBRARY = libio.a # LIBRARY has a trailing space.
missing_file:
touch $(LIBRARY)
ls-l | grep '$(LIBRARY)'
変数への代入方法はいくつかあります。比較的よく使うのは+=。これは変数に追記する記法になります。ちなみにプログラムではよくありがちな次の記法は間違いです。再起的に展開しようとするので、ループになってしまいます。そのため追記は+=を使わないと実現できません。
recursive = $(recursive) new stuff
$ make
Makefile:3: *** Recursive variable `recursive' references itself (eventually). Stop.
変数の定義箇所も複数あり、それを適切に使い分けると様々な環境の違いに適応できるように書けます。
-
ファイル内で定義
すでに出てるやつなので省略。 -
コマンドライン
コマンドラインからも変数を上書きできます。個人ごとの開発環境の違いなどはこれで対応するのがよいです。
$ make CFLAGS =-g CPPFLAGS ='-DBSD-DDEBUG'
-
環境変数
環境変数の値も全てmakeで使えます。定義してある環境変数を見て環境をチェックして条件分岐をするというのがよくある流れです。 -
makeの定義変数
makeに定義ずみの変数もあります。make自身も MAKE って変数で定義されてます。この辺を使いこなせるとちょっとカッコいい。
マクロ・コマンド
マクロも使うことができます。あくまで関数ではなくマクロです。あまり多用するとスパゲッティーコード化するので、なるべく最小範囲で使うことを推奨します。
まずは条件分岐。前に出てきた環境変数をチェックして環境ごとに切り替える使い方でよく使います。
ifdef COMSPEC
PATH_SEP := ;
EXE_EXT := .ex
else
PATH_SEP := :
EXE_EXT :=
endif
関数も使えます。個人的にはあまり関数は使わず、シェルスクリプトに記載した方が良いと思う。
define create-jar
@echo Creating $@...
$(RM) $(TMP_JAR_DIR)
$(MKDIR) $(TMP_JAR_DIR)
$(CP)-r $^ $(TMP_JAR_DIR)
cd $(TMP_JAR_DIR) && $(JAR) $(JARFLAGS) $@ .
$(JAR)-ufm $@ $(MANIFEST)
$(RM) $(TMP_JAR_DIR)
endef
シェルコマンドを使うときに、その挙動を調整することもできます。
- @
makeコマンドではコマンド実行前にコマンドを表示しますが、コマンドの前に@をつけるとコマンド実行を表示しなくなります。echoと合わせてよく用います。
hello: hello.c
echo 'build hello1'
@echo 'build hello2'
gcc hello.c -o hello
$ make hello
echo 'build hello1'
build hello1
build hello2
gcc hello.c -o hello
コマンドでエラーが発生しても無視する。実行が成功しても失敗しても事後クリーンナップが必要な時などに、失敗処理で終了しないようにするためなどに使います。
プロジェクトでのMakefile管理
ソースコードが大きくなってきた場合、Makefileをディレクトリごとに適切に切り分けて管理すると見通しやすくなります。その際に最適なmake実行を各ディレクトリに移動しながら再起的に実行していくことになります。その際はmakeは--directory=でディレクトリを指定して実行すると使いやすいです。cdして実行するコマンドを書くより、こっちを使った方がキレイにかけるかと。
hello:
@echo 'build top'
$(MAKE) --directory=dir1 hello
$(MAKE) --directory=dir2 hello
$ make hello
build top
/Library/Developer/CommandLineTools/usr/bin/make --directory=dir1 hello
hello
/Library/Developer/CommandLineTools/usr/bin/make --directory=dir2 hello
hello
まとめ
ざっくりMakefileの使い方を見てみました。特にphony targetを使ったタスクランナーとしてのMakefileの使い方を見てきました。
Makefileだと言語や環境への依存が少なく動かせるため、マイクロサービスなどで複数の言語を使ったシステムを連携させて開発するときとかに便利ですね。