これは、なにをしたくて書いたもの?
Goでのビルドツールは、makeを使うことが多いと聞いたので。
いくつか、Goで書かれた有名なOSSを見てみると、確かにMakefileが置かれています。
https://github.com/prometheus/prometheus/blob/v2.24.1/Makefile
https://github.com/hashicorp/terraform/blob/v0.14.5/Makefile
https://github.com/moby/moby/blob/v20.10.2/Makefile
https://github.com/kubernetes/kubernetes/blob/v1.20.2/build/root/Makefile
Goの勉強を始めていることですし、これを機に少し覚えてみようかな、と。
make自体は使ったことがあります。ただ、すでに用意されているMakefileと手順に従い、make
、make install
とかを
実行してきただけですね。
環境
今回の環境は、こちらです。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal $ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
make自体は、インストール済みです。
$ make --version GNU Make 4.2.1 このプログラムは x86_64-pc-linux-gnu 用にビルドされました Copyright (C) 1988-2016 Free Software Foundation, Inc. ライセンス GPLv3+: GNU GPL バージョン 3 以降 <http://gnu.org/licenses/gpl.html> これはフリーソフトウェアです: 自由に変更および配布できます. 法律の許す限り、 無保証 です.
make
makeについて見ていくということで、まずは一次リソースをあたりましょう。
GNUプロジェクトを見に行きます。
Software - GNU Project - Free Software Foundation
こちらにmakeの説明があります。ちょっと概要を見てみましょう。正確には、GNU Makeですね。
Make - GNU Project - Free Software Foundation
makeは、プログラムのソースファイルから、実行可能ファイルやその他の非ソースファイルの生成をコントロールするツールです。
プログラムをビルドするための方法は、makefileと呼ばれるファイルに記述します。
makeの機能は、以下になります。
- パッケージのビルド方法やインストール方法がmakefileに記述されているため、ユーザーは詳細を知らなくてもこれらのタスクを実行できる
- makeはファイルの更新を把握し、前回のビルドから変更があったファイルに基づいて処理を行い、必要なファイルを更新する
- 言語非依存で、makefile内で指定されたシェルコマンドを実行する
- makeはパッケージの作成以外にも、インストールやアンインストールなど、様々な処理を行わせることができる
makefileには、ソースコードからビルド結果を生成するための、一連のルールを書きます。この時、ルールには依存関係を持たせる
ことができます。
以下は、簡単なルールの構文です。
target: dependencies ... commands ...
makefile内にはルール(target)を書き、makeでどのルールを実行するかを指定することができます。
…というのが、基本的な話のようですね。
makeについての詳細は、マニュアルを見ていくことになります。
GNU Make Manual - GNU Project - Free Software Foundation
https://www.gnu.org/software/make/manual/make.html
では、これらを見ながら簡単に試していってみましょう。
If you are new to make, or are looking for a general introduction, read the first few sections of each chapter, skipping the later sections.
はい。
The exception is the second chapter, An Introduction to Makefiles, all of which is introductory.
つまり、初めての人は2章を読みなさいということのようです。
はじめてのMakefile
というわけで、こちらを見ながら始めていきます。
単純なmakefileは、以下のようなシンプルなルールで構成されます。
target … : prerequisites … recipe … …
ここで、ターゲット(target
)には実行可能ファイルの名前やオブジェクトファイル、アクションの名前を指定します。
main.o
、clean
、install
、build
などですね。
前提条件(prerequisites
)として、別のターゲットを指定することができます。これはターゲット間の依存関係を
表します。
レシピ(recipe
)は、このターゲットで実際に行う処理(コマンド)を記述します。
注意点として、レシピの先頭にはタブを配置する必要があります。
実際に、makefileを書いてみましょう。
Makefile
step1: step2 step4 echo 1 echo one step2: step3 echo 2 step3: echo 3 \ three step4: echo 'four!!'
makefileはMakefile
という名前でmake
コマンドは認識しますが、-f
オプションで別の名前を指定することもできます。
step1はstep2、step4を前提にしていて、step2はstep3を前提にしています。step3には前提条件はありません。
step1には複数のコマンド(レシピ)を書いています。step3は、レシピを継続行にしています。
この状態で、make
コマンドを実行してみます。
$ make echo 3 \ three 3 three echo 2 2 echo 'four!!' four!! echo 1 1 echo one one
step3 → step2 → step4 → step1という実行順になりました。
なんとなく、依存関係を解決しつつ実行しているのがわかると思います。
make
コマンドの引数としてターゲットを指定することで、各ターゲット単位(と依存するターゲット)ごとにも実行できます。
$ make step3 echo 3 \ three 3 three $ make step4 echo 'four!!' four!! $ make step2 echo 3 \ three 3 three echo 2 2
ところで、単にmake
と指定した時は、step1のターゲットが実行されたように思います。
この理由は、以下に書いてあります。
By default, make starts with the first target (not targets whose names start with ‘.’). This is called the default goal.
つまり、最初のターゲットが実行されるようです。これをデフォルトゴールと呼びます、と。
これをオーバーライドする方法として、コマンドの引数で指定する方法(先ほど使った方法)と.DEFAULT_GOAL
という特殊変数を
使う方法があるようです。
You can override this behavior using the command line (see Arguments to Specify the Goals) or with the .DEFAULT_GOAL special variable (see Other Special Variables).
試しに、.DEFAULT_GOAL
を指定してみましょう。
Makefile
.DEFAULT_GOAL := step4 step1: step2 step4 echo 1 echo one step2: step3 echo 2 step3: echo 3 \ three step4: echo 'four!!'
確認。
$ make echo 'four!!' four!!
明示的にターゲットを指定しない場合に、実行されるターゲットが変わりましたね。
ビルドで使うようなmakefileの例を見てみると、ターゲットの名前はファイル名になっていたりします。
edit : main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o cc -o edit main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o main.o : main.c defs.h cc -c main.c kbd.o : kbd.c defs.h command.h cc -c kbd.c command.o : command.c defs.h command.h cc -c command.c display.o : display.c defs.h buffer.h cc -c display.c insert.o : insert.c defs.h buffer.h cc -c insert.c search.o : search.c defs.h buffer.h cc -c search.c files.o : files.c defs.h buffer.h command.h cc -c files.c utils.o : utils.c defs.h cc -c utils.c clean : rm edit main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o
makefileは、このファイル名をターゲットにすることで依存関係を表現します。
今回作成したstep1のようなターゲットは、ファイルではありません。上記の例だとcleanがそうですね。
このようなファイルではないターゲットをPhonyターゲットと呼ぶそうです。
Phonyターゲットは、ファイル名ではないターゲットのことです。これを使う理由は、同じ名前のファイルとの競合を回避するためと、
パフォーマンスの向上のためです。
レシピがターゲットとなるファイルを作成しない場合、ターゲットが再作成される度にレシピが実行されるようです。
また、ターゲットと同じ名前のファイルがあるとうまく機能しなくなるようです。
とすると、今回のmakefileはすべてPhonyターゲットなので、以下のようにするのが正解でしょうね。
Makefile
# .DEFAULT_GOAL := step4 .PHONY: step1 step1: step2 step4 echo 1 echo one .PHONY: step2 step2: step3 echo 2 .PHONY: step3 step3: echo 3 \ three .PHONY: step4 step4: echo 'four!!'
変数を使う
makefile内では、変数を使うこともできます。
Variables Make Makefiles Simpler
こんな感じで、=
で定義します。参照は$(変数)
です。
Makefile
ECHO_COMMAND = echo MESSAGE = 'Hello World' .PHONY: hello hello: $(ECHO_COMMAND) $(MESSAGE)
確認。
$ make echo 'Hello World' Hello World
ちなみに、この変数は外部から上書きすることもできます。
$ make MESSAGE=hello echo hello hello
これは、makeコマンド自身に上書きしてもらっています。
環境変数で上書きする場合は、単純に指定しただけではダメです。
$ MESSAGE=hello make echo 'Hello World' Hello World
-e
オプションを付ける必要があります。
$ MESSAGE=hello make -e echo hello hello
Variables from the Environment
変数については、ドキュメントのこのあたりを見ましょう。
また、自動で定義される変数もあります。
Goプロジェクトでmakefileを書いてみる
最後にGoプロジェクトで簡単に試してみましょう。
Goのバージョン。
$ go version go version go1.15.7 linux/amd64
プロジェクト作成。
$ go mod init app go: creating new go.mod: module app
go.mod
module app go 1.15
ソースコードを作成。
main.go
package main import ( "fmt" ) func main() { fmt.Println(GetMessage()) } func GetMessage() string { return "Hello World!!" }
テストコードも作成。
main_test.go
package main import ( "testing" ) func TestGetMessage(t *testing.T) { if GetMessage() != "Hello World!!" { t.Error("test failed!") } }
makefileも書きます。
Makefile
EXECUTABLE_FILE = app .PHONY: build build: format lint test go build .PHONY: run run: format go run main.go .PHONY: format format: gofmt -w -d . .PHONY: lint lint: go vet ./... .PHONY: test test: go test ./... .PHONY: clean-testcache clean-testcache: go clean -testcache .PHONY: clean clean: rm $(EXECUTABLE_FILE)
デフォルトでは、buildターゲットを実行します。ターゲットの依存関係から、フォーマットしてgo vet
をかけて、テストを実行して
実行可能ファイルの生成まで行います。
$ make gofmt -w -d . go vet ./... go test ./... ok app 0.002s go build
できあがったファイルの実行。
$ ./app Hello World!!
clean。こちらは単発のターゲットです。
$ make clean rm app
run。フォーマットとプログラムの実行が含まれます。
$ make run gofmt -w -d . go run main.go Hello World!!
依存するターゲットを順次実行している途中で、あるターゲットが失敗するとそこで停止します。たとえば、今回のデフォルトターゲットを
実行して、go test
が失敗した場合はこんな感じになります。
$ make gofmt -w -d . go vet ./... go test ./... --- FAIL: TestGetMessage (0.00s) main_test.go:9: test failed! FAIL FAIL app 0.002s FAIL make: *** [Makefile:21: test] エラー 1
また、-n
を付けると実際には処理を行わずにどのようなターゲットが実行されるかを表示してくれます。
$ make -n gofmt -w -d . go vet ./... go test ./... go build $ make -n test go test ./... $ make -n run gofmt -w -d . go run main.go
なんとなく、雰囲気はわかった気がします。