Go でツール書くときの Makefile 晒す

Go でツール書くときはタスクランナーとして make を使っています。ビルドだけじゃなくて、テストや配布用パッケージ作成も一括して make でやっています。
今回は整理も兼ねて、自分が普段どういう `Makefile` を使っているのか解剖していきます。

## なぜ make を使うのか

ビルドフラグ覚えるのが面倒だから、make は (Windows を除く) 大半のプラットフォームに入っていて使いやすいからというのが理由です。`script/build` みたいにシェルスクリプトを複数用意するのでもまあ良いと思いますが…。大半の Go プロジェクトは `Makefile` 置いてありますね。

## make を使った開発フロー

基本的には、リポジトリを `git clone` / `go get -d` した後に以下のコマンドを打てばアプリケーションをインストールできるようにしています。

```bash
$ cd $GOPATH/src/github.com/yourname/yourapp
$ make deps
$ make
$ make install
$ yourapp
```

以前 `make deps` は余分で `make && make install` で完結してほしいと言われたことがあったのですが、`glide install` するオーバーヘッドが（変更なくとも）多少あったので `deps` は入れました。Kubernetes 系とかデカいの依存してるとね… LL だと `bundle install` だの `npm install` だのを独立に行うので、まあいいのではないでしょうか。

## 変数定義

```makefile
NAME     := s3url
VERSION  := v0.3.1
REVISION := $(shell git rev-parse --short HEAD)

SRCS    := $(shell find . -type f -name '*.go')
LDFLAGS := -ldflags="-s -w -X \"main.Version=$(VERSION)\" -X \"main.Revision=$(REVISION)\" -extldflags \"-static\""
```

アプリケーション名やバージョン番号といった変数定義です。

`SRCS` は、リポジトリに含まれる Go のファイルをすべて引っ張ってきています。
`-ldflags` は、バージョン番号と Git commit hash をバイナリに埋め込む用途で利用しています。この場合、`main` パッケージに以下のような変数定義があると `-ldflags` 経由でその値を書き換えることができます。

```go
package main

var (
	Version  string
	Revision string
)
```

詳しくは下の記事を御覧ください。

[Go言語: ビルド時にバージョン情報を埋め込みたい - Qiita](http://qiita.com/suin/items/d643a0ccb6270e8e3734)

たまにあるのですが、ビルド時間 (`date`) をバイナリに埋め込むのはよくないです。ソースコードが一切変わってなくともビルドする度にバイナリの中身が変わってしまい (checksum も変わる)、ビルド再現性が失われるためです。せいぜいバージョン番号、リビジョン、`go version` くらいにしておきましょう。

`-extldflags` に関しては後述します。

## 開発環境セットアップ

依存パッケージ管理には [Glide](https://github.com/Masterminds/glide) を使っています。`go get` で済ますパターンもありますが、個人的に依存ライブラリはリビジョンを固定して使いたい派です。

### `make glide`

```makefile
.PHONY: glide
glide:
ifeq ($(shell command -v glide 2> /dev/null),)
	curl https://glide.sh/get | sh
endif
```

Glide 自体をインストールするターゲットです。すでに Glide がインストールされているのであれば何もしません。
各プラットフォーム対応な公式のインストールスクリプトを使っているので、Homebrew で入れたい人はこれを使わず別途インストールしてください。

### `make deps`


```makefile
.PHONY: deps
deps: glide
	glide install
```

Glide を使って依存ライブラリをインストールするターゲットです。Glide インストールされてなければ `make glide` でインストールします。

## バイナリビルド

### `make` (= `make bin/NAME`)

```makefile
bin/$(NAME): $(SRCS)
	go build -a -tags netgo -installsuffix netgo $(LDFLAGS) -o bin/$(NAME)
```

`go build` してバイナリを生成するターゲットです。依存ターゲットに `SRCS` を含めておくことで、手元の Go プログラムが書き換わった場合__のみ__ビルドを実行するようにします。

`-a -tags netgo -installsuffix netgo` のくだりは見覚えがないかもしれません。これは、`go build` で必ず __static link__ のバイナリを生成するためのおまじないです。先程の `-ldflags='-extldflags="static"'` と一緒に使います。
Go 1.4 より、`net` パッケージを使うアプリケーションはデフォルト __dynamic link__ でビルドされるようになりました。これ何が困るかというと、Alpine Linux みたいな空っぽの環境にそのバイナリ1個持っていっても動かないんですよね。dynamic link で見に行くライブラリがインストールされてないから。こちらはバイナリのポータビリティ重視で Go 使ってるところがあるので、超軽量 Linux であろうとバイナリポン置きで動いてほしいのです。というわけで、依存物をすべてバイナリに含める static link でビルドさせたいのです。
`net` パッケージは自分が明示的に使わなくても依存ライブラリが使ってることが多々あるので、もう標準でこれらのフラグを書いておくのが安心です。

このあたりの話は、下の記事が詳しいです。

[golangで書いたアプリケーションのstatic link化 - okzkメモ](http://okzk.hatenablog.com/entry/2016/08/03/234738)

### `make install`

```makefile
.PHONY: install
install:
	go install $(LDFLAGS)
```

`$GOPATH/bin` にバイナリをインストールするターゲットです。開発中はそうそう使うことないと思いがちです。しかし、[gocode](https://github.com/nsf/gocode) が `go install` で生成されるファイルを読みに行く仕様になっているため、新しいメソッドやパッケージを追加したら都度実行しないと、それらがエディタ補完に出てこなくなる罠があります。

### `make clean`

```makefile
.PHONY: clean
clean:
	rm -rf bin/*
	rm -rf vendor/*
```

生成物と依存ライブラリを一掃するターゲットです。

## テスト

### `make test`

```makefile
.PHONY: test
test:
	go test -cover -v `glide novendor`
```

ふつうにテストを実行するターゲットです。`glide novendor` で、`vendor` ディレクトリに保存された依存ライブラリを除く、つまり自分の書いたアプリケーションコードのみをテスト対象にしています。

### `make ci-test`

```makefile
.PHONY: ci-test
ci-test:
	echo "" > coverage.txt
	for d in `glide novendor`; do \
		go test -coverprofile=profile.out -covermode=atomic -v $$d; \
		if [ -f profile.out ]; then \
			cat profile.out >> coverage.txt; \
			rm profile.out; \
		fi; \
	done
```

Travis CI でテストを走らせるのに使うターゲットです。

`go test` はテストカバレッジの出力をサポートしていますが、複数パッケージ同時に `go test` するとパッケージ単位でカバレッジが出力されます（アプリケーション単位ではない）。また、ファイルにカバレッジを出力する `-coverprofile` オプションは、複数ターゲットに対応していません。
テストカバレッジの管理には [Codecov](https://codecov.io/) を使っているのですが、そこではパッケージごとにカバレッジを出力してひとまとめにする方法が紹介されていました。これを頑張って `Makefile` に移植して使っています。
https://github.com/codecov/example-go#caveat-multiple-files

## バイナリ配布

作ったツールは適宜 git タグを打って、Travis CI から各プラットフォーム対応のバイナリを GitHub Releases へアップロードするようにしています。

### `make cross-build`

```makefile
.PHONY: cross-build
cross-build: deps
	for os in darwin linux windows; do \
		for arch in amd64 386; do \
			GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo $(LDFLAGS) -o dist/$$os-$$arch/$(NAME); \
		done; \
	done
```

{Mac, Linux, Windows} の {32bit, 64bit} 対応バイナリ全6種類を一括で生成するターゲットです。主に CI 上で使います。最近の Go は `GOOS` と `GOARCH` 渡すだけでクロスコンパイルできるのが便利ですね…
クロスコンパイルの時は `CGO_ENABLED=0` で cgo を無効化しています。[dtan4/k8sec](https://github.com/dtan4/k8sec) では、上述した netgo 周りのフラグだけだとダメで cgo 無効化しないと static link でビルドされませんでした。


[公式ドキュメント](https://golang.org/cmd/cgo/) にもクロスコンパイル時は cgo 無効化すると書いてあります。

> The cgo tool is enabled by default for native builds on systems where it is expected to work. It is disabled by default when cross-compiling. You can control this by setting the CGO_ENABLED environment variable when running the go tool: set it to 1 to enable the use of cgo, and to 0 to disable it. The go tool will set the build constraint "cgo" if cgo is enabled.

### `make dist`

```makefile
DIST_DIRS := find * -type d -exec

.PHONY: dist
dist:
	cd dist && \
	$(DIST_DIRS) cp ../LICENSE {} \; && \
	$(DIST_DIRS) cp ../README.md {} \; && \
	$(DIST_DIRS) tar -zcf $(NAME)-$(VERSION)-{}.tar.gz {} \; && \
	$(DIST_DIRS) zip -r $(NAME)-$(VERSION)-{}.zip {} \; && \
	cd ..
```

`make cross-build` で生成した各プラットフォームのバイナリをそれぞれ `.tar.gz` と `.zip` にまとめるターゲットです。ここで生成されたアーカイブファイルを GitHub Releases にアップロードして配布するようにしています。

Travis CI から GitHub Releases に上げるスマートなやり方は、下の記事を御覧ください。

[Travis CI から複数ファイルを GitHub Releases にアップロードする - Qiita](http://qiita.com/dtan4/items/f2736c25c4eb63b2d206)

## Docker

作ったツールを Docker image で配布したいときの設定です。`Dockerfile` は Alpine Linux にバイナリ置くだけのシンプルなものです。

```dockerfile:Dockerfile
FROM alpine:3.4

RUN apk add --no-cache --update ca-certificates

COPY bin/k8sec /k8sec

ENTRYPOINT ["/k8sec"]
```

### `make docker-build`

```makefile
.PHONY: docker-build
docker-build:
ifeq ($(findstring ELF 64-bit LSB,$(shell file bin/$(NAME) 2> /dev/null)),)
	@echo "bin/$(NAME) is not a Linux 64bit binary."
	@exit 1
endif
	docker build -t $(DOCKER_IMAGE) .
```

Docker image をビルドするターゲットです。置くバイナリは (64bit OS で作業するのであれば) Linux 64bit 向け `GOOS=linux GOARCH=amd64` でないといけないので、`file` コマンドでバイナリフォーマットを確認するようにしています。

```bash
$ file bin/k8sec
bin/k8sec: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
```

### `make ci-docker-release`

```bash
DOCKER_REPOSITORY := quay.io
DOCKER_IMAGE_NAME := $(DOCKER_REPOSITORY)/dtan4/k8sec
DOCKER_IMAGE_TAG  ?= latest
DOCKER_IMAGE      := $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)

.PHONY: ci-docker-release
ci-docker-release: docker-build
	@docker login -e="$(DOCKER_QUAY_EMAIL)" -u="$(DOCKER_QUAY_USERNAME)" -p="$(DOCKER_QUAY_PASSWORD)" $(DOCKER_REPOSITORY)
	docker push $(DOCKER_IMAGE)
```

Travis CI から [Quay.io](https://quay.io/) に Docker image を push するターゲットです。`DOCKER_QUAY_` のつく環境変数は `.travis.yml` に暗号化して入れるか Web 上で入れておくかしておきます。
make は基本実行するコマンド自体を表示するのですが、コマンド頭に `@` をつけると表示されないようにできます。パスワードが CI 画面上に露出したらまずいので、`docker login` は `@` つけてます。

## おわりに

普段自分が Go 書くのに使っている `Makefile` を紹介しました。新しくツール作るときは既存のリポジトリから `Makefile` をコピーして…ってしているので、どうにかしたいところです。
あと、自分 `Makefile` 書き始めたのが Go 書き始めたのと同時期なので、まだまだ make 力が低いです。この記事でも何かおかしいところがあれば、コメント等で教えていただけると助かります。

最後に、今回のスニペットの元ネタとなった拙作 `Makefile` たちを置いておきます。

- https://github.com/dtan4/k8sec/blob/master/Makefile
- https://github.com/dtan4/k8stail/blob/master/Makefile
- https://github.com/dtan4/s3url/blob/master/Makefile

## REF

`Makefile` 作るに当たっては、いろんなリポジトリの `Makefile` を参考にしました。特に Glide の `Makefile` は参考にさせてもらいました。

- [A makefile for Golang CLI tools · ariejan de vroom](https://ariejan.net/2015/10/03/a-makefile-for-golang-cli-tools/)
- [textql/Makefile at master · dinedal/textql](https://github.com/dinedal/textql/blob/master/Makefile)
- [glide/Makefile at master · Masterminds/glide](https://github.com/Masterminds/glide/blob/master/Makefile)