前置き
Go 言語を学習している。
Go はコンパイル言語なので、実行前にバイナリをビルドする必要がある。
ビルドコマンドは go build である。
しかし実際のプロジェクトでは、いきなり go build を実行するのではなく、
次のような処理を順番に実行することが多いようだ。
- コードフォーマット (
go fmt) - 静的解析 (
go vet) - テスト (
go test) - ビルド (
go build)
これらの処理を決まった順序で繰り返し実行できるようにするためのツールとしてよく使われるのが make である。
"Go developers have adopted
makeas 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
.PHONYline keepsmakefrom 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.cやutil.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.c は app より新しいので再ビルド実行
となる。
なぜこのような作りになっている?
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 を中心に紐解くと、なんのためのツールなのかがよく分かった。
--
藤代