CLOVER🍀

That was when it all began.

makeを学んでみる

これは、なにをしたくて書いたもの?

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

では、これらを見ながら簡単に試していってみましょう。

How to Read This Manual

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章を読みなさいということのようです。

An Introduction to Makefiles

はじめてのMakefile

というわけで、こちらを見ながら始めていきます。

An Introduction to Makefiles

単純なmakefileは、以下のようなシンプルなルールで構成されます。

target … : prerequisites …
        recipe
        …
        …

What a Rule Looks Like

ここで、ターゲット(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!!'

A Simple Makefile

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.

How make Processes a Makefile

つまり、最初のターゲットが実行されるようです。これをデフォルトゴールと呼びます、と。

これをオーバーライドする方法として、コマンドの引数で指定する方法(先ほど使った方法)と.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の例を見てみると、ターゲットの名前はファイル名になっていたりします。

A Simple 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 Targets

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

変数については、ドキュメントのこのあたりを見ましょう。

How to Use Variables

また、自動で定義される変数もあります。

Automatic Variables

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

なんとなく、雰囲気はわかった気がします。

あとはOSSで使われているmakefileを眺めたりして、覚えていきましょう。