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

  • 136
    いいね
  • 0
    コメント

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

なぜ make を使うのか

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

make を使った開発フロー

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

$ 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 だのを独立に行うので、まあいいのではないでしょうか。

変数定義

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 経由でその値を書き換えることができます。

package main

var (
    Version  string
    Revision string
)

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

Go言語: ビルド時にバージョン情報を埋め込みたい - Qiita

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

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

開発環境セットアップ

依存パッケージ管理には Glide を使っています。go get で済ますパターンもありますが、個人的に依存ライブラリはリビジョンを固定して使いたい派です。

make glide

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

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

make deps

.PHONY: deps
deps: glide
    glide install

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

バイナリビルド

make (= make bin/NAME)

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メモ

make install

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

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

make clean

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

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

テスト

make test

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

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

make ci-test

.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 を使っているのですが、そこではパッケージごとにカバレッジを出力してひとまとめにする方法が紹介されていました。これを頑張って Makefile に移植して使っています。
https://github.com/codecov/example-go#caveat-multiple-files

バイナリ配布

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

make cross-build

.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 は GOOSGOARCH 渡すだけでクロスコンパイルできるのが便利ですね…
クロスコンパイルの時は CGO_ENABLED=0 で cgo を無効化しています。dtan4/k8sec では、上述した netgo 周りのフラグだけだとダメで cgo 無効化しないと static link でビルドされませんでした。

公式ドキュメント にもクロスコンパイル時は 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

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

Docker

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

Dockerfile
FROM alpine:3.4

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

COPY bin/k8sec /k8sec

ENTRYPOINT ["/k8sec"]

make docker-build

.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=and64 でないといけないので、file コマンドでバイナリフォーマットを確認するようにしています。

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

make ci-docker-release

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 に Docker image を push するターゲットです。DOCKER_QUAY_ のつく環境変数は .travis.yml に暗号化して入れるか Web 上で入れておくかしておきます。
make は基本実行するコマンド自体を表示するのですが、コマンド頭に @ をつけると表示されないようにできます。パスワードが CI 画面上に露出したらまずいので、docker login@ つけてます。

おわりに

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

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

REF

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

この投稿は Go (その3) Advent Calendar 20168日目の記事です。