Makefileを書き散らす

最近ちょっとC言語を触っててビルド周りがようわからんのでしんどくなった。

そもそもMakefileって何

コンパイルのコマンドやファイル間の依存関係を記述したもの。 手作業では面倒なコンパイルを肩代わりしてくれる。プログラマブルコンパイルを提供する。 普通のシェルスクリプトとの違いは、更新されたファイルのうち、関連のあるものだけを更新するので、コンパイル時間が短縮できる。 makeは特定の言語に依存したものではない。

Makefileは行指向->改行すると別の処理と認識

object_files = \
# foo.o \
bar.o \
baz.o \

以下のように解釈される

object_files = 
bar.o baz.o

ルールとターゲット

Makefileは基本的に以下のような構造で成り立っている。

[作りたいファイル名]:
  [そのファイルを作るためのコマンド行]

この1つの塊をルールと呼び、ルールに書いてある作りたいファイル名のことをターゲットと呼ぶ。

ex:

hello.txt:
  echo Hello World > hello.txt

タスク

ターゲットとして、実在しないファイルを指定することができる。 これは擬似ターゲットとかダミーターゲットとか呼ばれる。 便宜上タスク、もしくはタスクターゲットと呼んでも良い。 対義として、ファイルをファイルターゲットと呼ぶ。

.PHONY: [実行したいタスク名]

.PHONYはタスクターゲットを宣言するためのターゲット。 タスク名の前にもタブが必要。

ex:そのディレクトリの.classファイルを全て削除するターゲット。

.PHONY: clean
clean:
  rm -rf *.class

こうすると、make cleanで全ての.classファイルが削除される。

サンプル

/* hello.c */
#include <stdio.h>

int main(int argc, char* argv[]) {
  printf("Hello World!\r\n");
  return 0;
}

前述の通り、[ターゲット名: 依存ファイル名]で記述する。

//Makefile
hello:  hello.c
  gcc -o hello hello.c

makeコマンドでは、make targetnameでターゲット名を指定する事もできる。 ターゲット名を指定しない場合、Makefile内の先頭のターゲットを実行する。

よくあるOSSmake make installと2回makeさせられるのは、 1回目で一通りコンパイルし、2回目のmake install/usr/local/binに実行ファイルを配置する、みたいなことをしているようだ。

$ make
gcc -o hello hello.c

$ make
gcc -o hello hello.c
make: `hello' is up to date.

ファイルの更新日時などを見て、コンパイルが不要だったら何もしない。

分割コンパイル

/* hello.c */
#include <stdio.h>
#include "sample.h"

int main(int argc, char *argv[])
{
  printf("Hello World!\r\n");
  sayHello();
  return 0;
}
//sample.c
#include <stdio.h>
#include "sample.h"

void sayHello()
{
  printf("Hello\n");
}
//sample.h
#ifndef _SAMPLE_H_
#define _SAMPLE_H_
void sayHello();
#endif
//makefile
hello:  hello.c sample.c
    gcc -o hello hello.c sample.c

こうすると、分割されたファイルもちゃんと結合した状態でhelloファイルが出来上がる。 しかし、この場合どれかのファイルが更新されていると全て毎回コンパイルしてしまうので、Makefileを修正する。

//makefile
hello:  hello.o sample.o
    gcc -o hello hello.o sample.o

hello.o:    hello.c
    gcc -c hello.c

sample.o:   sample.c
    gcc -c sample.c

こうすると、更新されたファイルだけがちゃんとコンパイルされる。

中間ファイルの削除

上記のmakeを実行すると、.oファイルができて面倒。ので、これを削除するターゲットを定義してやる。

//makefile
#This is PHONY target.
#PHONY target means that this doesn't have any specified files or dirs.
#To prevent make-command from detecting "clean" file as target,
#.PHONY: targetname must be written like below.
#Otherwise, you'll see the output: "'clean' is up to date."
.PHONY: clean
clean:
    rm -f ./*.o
$ make clean

.oファイルが削除される。便利。

複数ターゲットの一括実行

こんなPhonyターゲットを書くと、一度で複数のバイナリファイルを生成できる。

//makefile
#This "all" target indicates dummy dependency between hello and hello2.
#Executing this target, you'll have 2 bin files by one target.
.PHONY: all
all: hello hello2

hello:  hello.o sample.o
    gcc -o ./bin/hello hello.o sample.o

hello.o:    ./src/hello.c
    gcc -c ./src/hello.c

sample.o:   ./src/sample.c
    gcc -c ./src/sample.c

hello2: hello2.o
    gcc -o ./bin/hello2 hello2.o

hello2.o:   ./src/hello2.c
    gcc -c ./src/hello2.c
$ make all

ヘッダファイルだけ更新された場合

ヘッダファイルが更新されても、Cファイルが更新されない限り再コンパイルは実行されない。この場合ヘッダファイルとCファイルの間で齟齬が生じる。 ヘッダファイルが更新されたらCファイルも再コンパイルしてほしい。

ので、明示的にMakefileでヘッダファイルについて記述してやる必要がある。

sample.o:    ./src/sample.c
    gcc -c ./src/sample.c

sample.o:   ./src/sample.h

これだけ。sample.oの生成時にヘッダファイルの更新を見るようになる。

マクロ

ファイル名やコマンド名を直接書かなくてよくなるやつ。変数っぽい。

macro_name = file.o file2.oなどで定義して、$(macro_name)で利用可能。 デフォルトで定義されているマクロもあり、それらは上書き可能。

例えば、CCにはデフォルトでccが格納されているが、

CC = g++

と書くと、$(CC)でg++が実行されるようになる。

#Macro
objs = hello.o sample.o
CC = g++

#This "all" target indicates dummy dependency between hello and hello2.
#Executing this target, you'll have 2 bin files by one target.
.PHONY: all
all: hello hello2

hello:  $(objs)
    $(CC) -o ./bin/hello $(objs)

hello.o:    ./src/hello.c
    $(CC) -c ./src/hello.c

sample.o:   ./src/sample.c
    $(CC) -c ./src/sample.c

sample.o:   ./src/sample.h

hello2: hello2.o
    $(CC) -o ./bin/hello2 hello2.o

hello2.o:   ./src/hello2.c
    $(CC) -c ./src/hello2.c
$ make
g++ -c ./src/hello.c
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
g++ -c ./src/sample.c
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
g++ -o ./bin/hello hello.o sample.o
g++ -o ./bin/hello2 hello2.o

全てg++で実行されているのがわかる。(ちなみにコンパイルしてるのはCファイルなのでg++からは普通に怒られる)

内部マクロ

内部マクロはターゲット内で自動で定義される変数のようなもの。 ターゲット名やファイル名などをターゲット内で動的に使いたいときに利用する。

hello: $(objs)
    $(CC) -o $@ $(objs)

ここで$@という表記があるが、これはターゲット名、つまりhelloを表している。 実行時には、以下のように解釈される。

hello: $(objs)
    $(CC) -o hello $(objs)

次の例。

hello.o: hello.c
    $(CC) -c $<

$<は依存ファイルの先頭のファイル名を表す。この場合はhello.c

hello.o: hello.c
    $(CC) -c hello.c

こうなる。

他にも色々ある。

# Makefile
program = hello
objs = hello.o sample.o
CC = gcc
CFLAGS = -g -Wall

$(program): $(objs)
  #$^はリストなので、ここではhello.oとsample.o
    $(CC) -o $(program) $^

hello.o: hello.c
    $(CC) $(CFLAGS) -c $<

sample.o: sample.c
    $(CC) $(CFLAGS) -c $<

.PHONY: clean
clean:
    $(RM) $(program) $(objs)

サフィックスルール

ファイルの拡張子(suffix)ごとにルールを定義するもの。

#この行にはルールを適用する拡張子のリストを書く
.SUFFIXES:  .o .c

#実際のルールはこれ
#拡張子.oのファイルが拡張子.cのファイルに依存していることを表す。
.c.o:
  $(CC) $(CFLAGS) -c $<

おわりに

#include "../path/to/lib.h"とかじゃダメ?ダメか〜