ビデオリサーチ公式テックブログ

ビデオリサーチ公式テックブログ

make にまつわる物語

前置き

Go 言語を学習している。

Go はコンパイル言語なので、実行前にバイナリをビルドする必要がある。
ビルドコマンドは go build である。

しかし実際のプロジェクトでは、いきなり go build を実行するのではなく、
次のような処理を順番に実行することが多いようだ。

  • コードフォーマット (go fmt)
  • 静的解析 (go vet)
  • テスト (go test)
  • ビルド (go build)

これらの処理を決まった順序で繰り返し実行できるようにするためのツールとしてよく使われるのが make である。

"Go developers have adopted make as their solution. It lets developers specify a set of operations that are necessary to build a program and the order in which steps must be performed."

--Learning Go (Second Edition)

make とは

CLI の実行コマンドの順序を定めて実行するツール
...だと思っていた。
調べてみると違った。
make がなんなのかについては改めて最後にまとめるとして、一旦、どういう動作をするのかを説明していきたい。

...

make の実行には Makefile が必要である。
Makefile は、make コマンドに対して「何をどの順序で実行するか」を定義する設定ファイルである。
例えば、以下のような Makefile があるとする。

Makefile

.DEFAULT_GOAL := build

.PHONY:fmt vet build
fmt:
    go fmt ./...

vet: fmt
    go vet ./...

build: vet
    go build ./...

target

まずは、target について説明する。
最初の数行をとばして、コロン(:) がついている単語の部分をみてほしい。

fmt:
    go fmt ./...

vet: fmt
    go vet ./...

build: vet
    go build ./...

fmt:, vet:, build: といった部分は target と呼ばれる。
target のあとに、例えば vet: であれば、fmt という記載があるが、
こちらは target の前に実行されている必要がある別の target を指定している。
これにより、実行するコマンドの順序を指定することができる。
target のあとの行のインデントされた内容は target で実行される内容(コマンド)である。

make の実行

以下のようにCLIで実行できる。

$ make <target>

<target> はオプションなので指定する必要はなく、以下のようにもできる。

$ make

make vet を実行した場合は以下のようになる。

$ make vet
go fmt ./...
go vet ./...

一行一行追って説明すると、

Makefile

vet: fmt

vet のターゲットのあとに fmt ターゲットが指定されているので、まずは fmt を実行。

標準出力

go fmt ./...

その後、vet の内容を実行。

Makefile

vet: fmt
    go vet ./...

標準出力

go vet ./...

DEFAULT_GOAL

DEFAULT_GOAL は、make 実行時に引数で target の指定がない場合、どの target を実行するかを指定することができる。
今回の Makefile の例だと以下のように DEFAULT_GOAL を指定している。

.DEFAULT_GOAL := build

この場合は

$ make

は以下と同義。

$ make build

やってみる。

$ make
go fmt ./...
go vet ./...
go build ./...

PHONY

phony とは "偽の" という意味である。
make において"偽の"とは、実際のファイルではない、という意味である。

.PHONY:fmt vet build

今読んでいる Learning Go (Second Edition) によると、

The .PHONY line keeps make from getting confused if a directory or file in your project has the same name as one of the listed targets.

プロジェクト内に同名のディレクトリかファイルがあった場合に混乱を避けるためとある。
これはどういうことなのか。

試しに fmt というファイルを作る。
中身は空。

$ ll
total 4.6M
-rw-r--r-- 1 fuji staff  131 Mar 12 17:09 Makefile
-rw-r--r-- 1 fuji staff    0 Mar 18 13:04 fmt
-rw-r--r-- 1 fuji staff   30 Oct  9 08:32 go.mod
-rw-r--r-- 1 fuji staff   80 Mar  7 23:14 hello.go
-rwxr-xr-x 1 fuji staff 2.4M Mar 12 16:30 hello_world

この状態で、Makefile.PHONY の行をコメントアウトし、

#.PHONY:fmt vet build

$ make fmt と実行すると、何が起こるのか見てみる。

$ make fmt
make: `fmt' is up to date.

...
これがどういう意味か全くわからない。
調べてみると、make がなんのために作られたのかに行き当たった。

make の振る舞い

make はファイルの更新日時を比較して target で記載している内容を実行するかどうか決める。
更新日時とは

ls -l

で確認できる最終更新日時のことである。

以下のような Makefile があるとしよう。

Makefile

app: main.c util.c
    gcc main.c util.c -o app

Makefile には app という target が記載されており、同名のファイルが作成される実行内容となっている。
この場合 make

app というファイルが main.cutil.c といった :(コロン) のあとの依存関係の target より古いか?

を問う。
もしこの答えが YES なら ビルドを行う。
NO であれば、何もしない。

さらに具体的には、
もし

app       modified at 10:00
main.c    modified at 09:00
util.c    modified at 09:30

であれば、app は依存関係の target より新しいので何もしない。

一方で、もし依存関係のファイルのほうが新しい場合、

app       modified at 10:00
main.c    modified at 11:00  ← changed
util.c    modified at 09:30

main.capp より新しいので再ビルド実行
となる。

なぜこのような作りになっている?

make は1976年に作られた。理由として

  • 巨大なC言語のプロジェクトの再コンパイルを避ける
  • 遅いマシンでのCPU実行時間を省く

ということがあった様子。

改めて .PHONY: について

先程の話に戻るが、fmt というファイルを作り、以下のように .PHONY をコメントアウトした状態で、

#.PHONY:fmt vet build

以下のように実行した場合

$ make fmt
make: `fmt' is up to date.

fmt というファイルが存在し、かつ依存関係も定義されていないため、make はこの target を「すでに最新」と判断し、実行しない。

Makefile (抜粋)

fmt:
    go fmt ./...

一方で、.PHONY: が指定してあるtarget はどうなるか。
これは

これらの target は実ファイルではありません。
そのため、ファイルの更新日時やファイルの存在チェックを行う必要がありません。

という意味になる。
そして、同名のファイルが存在していても、make は常にその target を実行する。

まとめ

改めて make とはなにかというと、

依存関係とファイルの更新日時に基づいて、必要な処理だけを実行するビルドツール

であると言える。

もともとは make については Learning Go (Second Edition) で2ページにも満たない内容で書かれていたのだが、
PHONY を中心に紐解くと、なんのためのツールなのかがよく分かった。

--
藤代