概要
- Makefile は自分の書いたコードのビルドの自動化に便利なため基本的な記法は知っておくとよい
- いちいちシェルの履歴を確認したりコマンドをミスする悲しいことがなくなる
- Makefile はビルドだけでなく実験やらなんやらの task runner としても便利
- ビルド・実験を回す・結果をグラフにする,などをまとめてやったりすることも可能
- 上記は shell script でも可能だがメリットとして
- あるタスクに対して依存関係が記述できる
- 実験タスクの依存にビルドタスクを記述すれば,実験タスクを実行したら自動でビルドしてくれる
- あるタスクについて,依存先が更新されていなければ実行しないという機能が標準で存在
- 二行上のような依存を記述していたときに,ソースコードの更新の有無でビルドの実行をする/しないを勝手に判断してくれる
- あるタスクに対して依存関係が記述できる
- 以下の説明は GNU Make に準拠して説明を行う。BSD Make の場合上手く動作しない記法も含まれる
Makefile の基本的な利用法
TARGET := prog
CC := /usr/bin/gcc
CFLAGS := -g -O2 -Wall
RM := /bin/rm
$(TARGET): main.c
$(CC) $(CFLAGS) $< -o $@
clean:
$(RM) -rf $(TARGET)
all: $(TARGET)
.PHONY: all clean
上記は最も基本的な Makefile の記述のひとつ。 ひとつひとつ見ていく
Makefile の変数
Makefile はシェル変数とは別に独自の変数を持つ。:=
で変数を定義し,$(VAR)
のように記述して展開する。 また,変数は =
でも定義が可能だが,:=
と =
は定義式の評価のタイミングが異なるなど差異が存在する。 具体的には =
を用いると値が決まるのが実際に参照されるときになる。 Makefile のタスクの中でシェルの環境変数($PATH など)を展開したい場合は $$PATH
のように $
をエスケープする必要がある。
Makefile の変数は :=
の代わりに +=
を使うことで,既に存在する変数に追記することができる。
また,Makefile(とくに GNU Make)には多種多様な関数や機能が用意されている。一例として,
SRC := main.c test.c test2.c
OBJS := $(SRC:.c=.o)
と記述すると,$(OBJS)
の中身は main.o test.o test2.o
になる。
Makefile のタスク
タスクは ターゲット: 依存先
という風に記述する。最初の節にある Makefile の例をみてみると,
$(TARGET): main.c
$(CC) $(CFLAGS) $< -o $@
と書いてある。この中に書いてある変数を全部展開すると,
prog: main.c
gcc -g -O2 -Wall main.c -o prog
となる。つまり,prog
という実行ファイルは,main.c
というファイルに依存しているという記述である。このタスクの中身は,タスクの依存関係の次の行からインデントしてシェルスクリプトを記述することになる。この例では
$(CC) $(CFLAGS) $< -o $@
となっている部分である。タスクが複数行ある場合は,
target: deps
mkdir -p test
cd test && touch testfile.txt
のように全てインデントを行なう。注意しなければならないのは,このインデントはソフトインデント(半角スペース)ではなくハードインデント(タブ文字)でなくてはならない。また,このタスクは実際には
target: deps
sh -c "mkdir -p test"
sh -c "cd test && touch testfile.txt"
のように一行ごとにシェルが別々に起動して実行されるため,
target: deps
mkdir -p test
cd test
touch testfile.txt
と記述すると意図通り動作せず失敗してしまう。
Makefile の疑似ターゲット
例示した Makefile では .PHONY: all clean
と記述してあり,また,依存が存在しないタスク clean
や $(TARGET)
を依存に指定するタスク all
が記述してある。.PHONY:
で指定したタスクは擬似的なターゲットである。
これは all
や clean
といった名前のファイルを作るタスクではないことを明示している。というのも,Makefile のタスクは一般的にターゲットと同じファイル名のファイルを生成するタスクを記述するツールであるためだ。
Makefile の挙動として,タスクのターゲットに指定しているファイル名が存在しないとき,または依存先が更新されていた場合,このどちらかが真であればタスクが実行されるということになっている。しかし,プロジェクトのビルドではなく,この clean
ターゲットのようにただのタスクを記述したいときにそれでは不便なため,このように .PHONY:
を用いて擬似的なターゲットを作ることで解決している。
ちなみに,make
コマンドは引数にターゲットを指定するが,引数を渡さずにただ make
とだけ打った場合は暗黙的に make all
を実行したことと同じと見なされる。
Makefile の特殊な変数
Makefile のタスクの例にて,$<
や $@
という変数を利用した。これは Makefile が自動的に定義する特殊な変数である。
TARGET: DEP1 DEP2
なんかのタスク
というタスクを用意したときに,$@
は TARGET
として展開される。また,$^
は DEP1
として展開され,$<
は DEP1 DEP2
という風に展開される。他にも自動変数は複数あるが,基本的にこのみっつを覚えておけば良い。
Makefile の発展的な記述方法:サフィックスルール
TARGET := prog
CC := gcc
CXX := g++
CFLAGS := -g -O2 -Wall
CXXFLAGS := $(CFLAGS)
LD := ld
LDFLAGS := -lc -lm -lpthread
SRCS := main.c library1.c library2.c
OBJS := $(SRCS:.c=.o)
.SUFFIXES:
.SUFFIXES: .c .o
.PHONY: all
all: $(TARGET)
.c.o:
$(CC) $(CFLAGS) -c $^ -o $@
$(TARGET): $(OBJS)
$(LD) $(LDFLAGS) $< -o $@
Makefile の発展的な記法として サフィックスルール が存在する。 上記の Makefile を見てほしい。
make
コマンドを実行したとき,自動で all
ターゲットを実行するため,この場合は all: $(TARGET)
という疑似ターゲットによる依存関係をみて,$(TARGET): $(OBJS)
という依存関係が記述されたターゲットのタスクを実行することになる。
しかし,この時依存関係には $(OBJS)
しか書いていない。この $(OBJS)
変数はこの定義だと main.o library1.o library2.o
と展開されるが,この .o
拡張子を持つオブジェクトファイルをどのソースコードからどう生成するかという依存関係は一切記述されていないのである。
これは実は,.c.o:
と書かれているターゲットで解決される。これをサフィックスルールと呼び,この場合,.o
という拡張子であれば自動的に同名で拡張子が .c
となっているファイルから生成するよ,ということを言っている。
このサフィックスルールの場合は gcc に -c
オプションを追加しているため,結果としてソースコードが 1 ファイル単位でオブジェクトファイルにコンパイルされ,$(TARGET): $(OBJS)
のタスクでは生成された全てのオブジェクトファイルを ld
コマンドによってリンカーがリンクしているのである。
この方法だと,複数のソースコードのうち一つのファイルだけが変更された時,そのファイルのコンパイルと全体のリンクだけが走り,全てのファイルを全てコンパイルしなおさなくて済む(=差分コンパイルが可能)という点でメリットがある。
Makefile の task runner としての利用方法
基本的には前章で説明した方法を応用し,疑似ターゲットを作りつつそのターゲットに依存関係を追加すれば,プロジェクトのビルド以外の仕事も依存関係を考慮して実行できるということになる。
次に示すのは Markdown で記述されたファイルを LaTeX へ変換した上で PDF へ typeset(コンパイル)するタスクと,make preview
によって生成した PDF を PDF ビューワーで閲覧するタスクが定義してある筆者作の Makefile である。
実際には LaTeX には latexmk
という Perl 製の便利な typeset スクリプトが標準に存在するため,実際に LaTeX を記述する際には PDF の閲覧にも typeset にもそちらを用いてほしい。あくまで Makefile の応用の一例である。
TEX=platex
PANDOC=pandoc
BIBTEX=pbibtex
DVI2PDF=dvipdfmx
TEX_FLAGS= --shell-escape -kanji=utf8 -kanji-internal=utf8 -interaction=batchmode
UNAME=$(shell uname)
ifeq $($(UNAME), Darwin)
VIEWER=open
else ifeq $($(UNAME), Linux)
VIEWER=xdg-open
else ifeq $($(OS), Windows_NT)
VIEWER=start
endif
TARGET=paper
COUNT=3
SOURCE = manuscript.tex
MDSCRIPTS = src/abstract.md src/contents.md
TEXFILES=$(MDSCRIPTS:.md=.tex)
DVIFILE=$(SOURCE:.tex=.dvi)
BIBFILES=cite/paper.bib
REFSTYLES=sty/crossref_config.yaml
PANDOC_FLAGS= --smart -f markdown+pipe_tables -t latex --filter pandoc-crossref --natbib
PANDOC_FILTER_FLAGS= -M "crossrefYaml=$(REFSTYLES)"
.SUFFIXES: .tex .md .pdf
.PHONY: all semi-clean clean preview
all: $(TARGET).pdf semi-clean
.md.tex:
@cat $< \
| $(PANDOC) --verbose $(PANDOC_FLAGS) $(PANDOC_FILTER_FLAGS) \
| sed 's/.png/.pdf/g' \
| sed 's/includegraphics/includegraphics[width=1.0\\columnwidth]/g' \
| sed 's/\[htbp\]/[t]/g' \
> $@
$(TARGET).pdf: $(TEXFILES)
@cd tex && for i in `seq 1 $(COUNT)`; \
do \
$(TEX) $(TEX_FLAGS) $(SOURCE); \
if [ ! -e "$(SOURCE:.tex=.blg)" ]; then \
$(BIBTEX) $(basename $(SOURCE)); \
fi \
done
cd tex && $(DVI2PDF) -o ../$(TARGET).pdf $(DVIFILE) 2> /dev/null
semi-clean:
@cd tex && rm -f *.aux *.log *.out *.lof *.toc *.bbl *.blg *.xml *.bcf *blx.bib *.spl
clean: semi-clean
@rm -f $(TARGET).pdf $(DVIFILE) $(TEXFILES)
preview:
@$(VIEWER) $(TARGET).pdf 2> /dev/null
ちなみに,タスクの先頭に @
がついてる行があるが,これを行うと実行したコマンドが表示されなくなる。