ShellScript
golang
Makefile
Make

Go言語開発を便利にするMakefileの書き方

Go言語開発での makeコマンド と Makefile

Go言語の開発ではmakeコマンドをタスク自動化ツールとしてよく使います。
よく使うコマンド、自動化したいタスクをMakefileに記述しておくと、開発に使う複雑なコマンドをすぐに実行したり、チームで共有出来ます。
Makefileに対して、難しいイメージを持っているかもしれませんが、超基本のMakefileの書き方はとてもシンプルなものです。

この記事の目的

  • Makefileの超基本がわかる
  • Go言語開発のタスク自動化ツールとしてのMakefileの書き方がわかる

前提知識

  • シェルスクリプト についての知識

書き方

書き始める前の準備

EditorConfigを設定して、タブ / スペース によるインデントのトラブルに会わないようにしましょう。
公式サイトにあなたのエディタが、EditorConfigをサポートしているか、プラグインの追加が必要かの一覧があります。
エディタのアイコンをクリックすればそれぞれのエディタ用のインストールページに飛びます。

.editorconfigファイルを次のように設定して、プロジェクトのルートディレクトリに置きます。

.editorconfig
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2

[*.go]
indent_style = tab
indent_size = 4

[Makefile]
indent_style = tab
indent_size = 4

この設定で、Go言語とMakefileのどちらもハードタブ 1 が適用されます。

Makefileを作成する

プロジェクトのルートディレクトリにMakefileというファイルを作成します。
拡張子は不要です。

コメントの書き方

コメントは行頭に # を書きます。

Makefile
# これはコメントです

タスクの実行方法

makeコマンドの引数にタスク名を渡します。
以下は task

make実行
$ make task

タスクを定義する

タスク定義のインデントは必ずタブで行います。

一番シンプルなタスク定義

Makefile
task:
    command

実行したコマンドを表示したくない場合

Makefile
task:
    @command

タスク定義が複数のコマンドの場合

Makefile
task:
    command1
    command2

ワンライナーで書く

Makefileの記法というより、シェルスクリプトの記法です。

Makefile
task:
    command1 && command2
Makefile
task:
    command1 ; command2 ;\
    command3

タスク定義が複数ある場合

Makefile
task1:
    command1
task2:
    command2

実行する場合は、タスク定義の名前を指定します。

make実行
$ make task2

タスク定義名が、プロジェクトのファイル名やディレクトリ名と同じ場合

makeの仕様でタスク定義名と同じファイルが存在している場合はタスクが実行されません。
これを回避するためには、.PHONY: task をタスク定義に付けます。

Makefile
.PHONY: task
task:
    command

あるタスク定義実行の前に別のタスク定義を依存タスクとして実行したい場合

go build の前に go getgo test を実行するなどが出来ます。

Makefile
task1:
    echo "task1実行"

task2: task1
    echo "task2実行"

task3: task1 task2
    echo "task3実行"
make実行結果
$ make task3
echo "task1実行"
task1実行
echo "task2実行"
task2実行
echo "task3実行"
task3実行

なお、task3の依存タスク定義を辿ると、 task1 が2回登場しますが、無駄な実行はされません。

自身のMakefileに定義してある別のタスク定義を実行したい場合

Makefile
task1:
    echo "task1実行"

task2:
    $(MAKE) task1
    echo "task2実行"
make実行結果
$ make task2
/Library/Developer/CommandLineTools/usr/bin/make task1
echo "task1実行"
task1実行
echo "task2実行"
task2実行

$(MAKE) ではなく、 make で書いてもこのケースでは動作しますが、
make task2 に付けたオプションも引き継がせるためには$(MAKE)を使います。

変数を使う

通常のシェルスクリプト変数(環境変数も含む)に加え、Makefile変数があります。

Makefile変数

固定値を変数に設定する

Makefile変数名=値 の形式でMakefile変数を定義します。

Makefile
VAR=hello make

Makefile変数をタスク定義で使うには、変数名を$()で囲みます。2

Makefile
VAR=hello make

hello:
    echo $(VAR)
make実行結果
$ make hello
echo hello make
hello make

シェル実行結果をMakefile変数に入れたい場合

$(shell command) を使用します。

Makefile
NOW=$(shell date)
task:
    echo $(NOW)
make実行結果
$ make task
echo Wed Oct 10 20:24:20 JST 2018
Wed Oct 10 20:24:20 JST 2018

シェルスクリプト変数・環境変数の扱い

シェルスクリプト変数・環境変数の参照

通常、シェルスクリプト変数・環境変数は $変数名 または ${変数名} で参照出来ますが、MakefileではMakefile変数としての解釈が優先されるため、
$${変数名} などのように$$を重ねて、 $をエスケープする必要があります。

Makefile
task:
    VAR="hello make" && echo $$VAR
make実行結果
$ make task
VAR="hello make" && echo $VAR
hello make

シェルスクリプト変数・環境変数の書き換え

タスク実行中に環境変数を書き換えたい場合は、コマンド実行が1行終わるごとに環境変数がmakeコマンド実行時に戻ることに注意します。

例えば、exportで一時的に環境変数を書き換えて何かをするタスクを複数コマンドに分けて記述したい場合は、 ;\ で改行してコマンドを複数書きます。

Makefile
task1:
    export VAR="この値は消えてしまいます"
    echo VAR=$${VAR}

task2:
    export VAR="この値は残ります" ;\
    echo VAR=$${VAR}
make実行結果
$ make task1
export VAR="この値は消えてしまいます"
echo VAR=${VAR}
VAR=

$ make task2
export VAR="この値は残ります" ;\
        echo VAR=${VAR}
VAR=この値は残ります

サブディレクトリのMakefileのタスク定義を実行する

サブディレクトリ./subdir/Makefile
sub-task:
    echo "hello"
ルートディレクトリMakefile
task:
    make -C subdir sub-task

make実行時に変数の値を渡す

make task 変数名=値 の形で変数を渡すことができます。

Makefile変数とシェルスクリプト変数の両方が上書きされます。

Makefile
VAR="これは上書きされる"
task:
    echo $(VAR) $$VAR
make実行結果
$ make task VAR=hello
echo hello $VAR
hello hello

応用パターンの紹介

シェルスクリプトのifを使う

golintがインストールされていなければインストールする例です。

Makefile
install-golint:
    @if ! type golint; then go get -u golang.org/x/lint/golint ; fi

複数行で書くこともできます。

Makefile
install-golint:
    @if ! type golint; \
        then go get -v -u golang.org/x/lint/golint ; \
    fi

Go言語情報の取得

go version, GOOS, GOARCHを取得してMakefile変数に入れる例です。

Makefile
GOVERSION=$(shell go version)
GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)

go test -v の結果に色をつける

go test -vの結果は色気がないですが、sed とANSIカラーで無理やり色をつけてみる例です。

# ANSI color
RED=\033[31m
GREEN=\033[32m
RESET=\033[0m

COLORIZE_PASS=sed ''/PASS/s//$$(printf "$(GREEN)PASS$(RESET)")/''
COLORIZE_FAIL=sed ''/FAIL/s//$$(printf "$(RED)FAIL$(RESET)")/''

test:
    go test -v ./... | $(COLORIZE_PASS) | $(COLORIZE_FAIL)

コマンドをテンプレート化して使い回す

go build コマンドを開発環境用と本番環境用のパラメータ切り替えして使い回すことを想定した例です。
ビルドタグも切り替えしています。

Makefile
BUILD_TAGS_PRODUCTION='production'
BUILD_TAGS_DEVELOPMENT='development unittest'

build-base:
    go build -o $(BIN_NAME) -tags '$(BUILD_TAGS) netgo' -installsuffix netgo -ldflags '-s -w' main.go

build-development:
    $(MAKE) build-base BUILD_TAGS=$(BUILD_TAGS_DEVELOPMENT) BIN_NAME=dev

build-production:
    $(MAKE) build-base BUILD_TAGS=$(BUILD_TAGS_PRODUCTION) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 BIN_NAME=prod 

lintタスクを定義する

シェルスクリプトのコマンド置換$()を使っていますが、 $ をエスケープして、$$()とする必要があります。

Makefile
lint:
    golint -set_exit_status $$(go list ./...)
    go vet ./...

終わりに

makeコマンド、 Makefileの仕様はここで紹介したもの以外にもたくさんあり、
私自身も使いこなせていない・よくわかっていない機能があります。
ですが、Go言語開発のお供のタスクランナーとして、よく使うコマンドを登録するくらいの用途であればMakefileは怖いものではありません。
どんどんタスクを登録して開発を便利にしていきましょう。


  1. インデントにタブ文字を使うこと。タブの代わりにスペースを2個、または4個入れてインデントすることを「ソフトタブ」と言います。 

  2. ${} で囲んでも$()と同じ動きになりますが、シェルスクリプトの変数と紛らわしくなるため、$()のみを使用します。