gex で Go プロジェクトの開発用ツールの依存を管理する

## TL;DR


- `go generate` で使うものや linter などの開発用ツールもバージョン固定したいよね
- [**gex**][gex] を使えば簡単にできるよ
- 独自の管理機構を持たない（dep / Modules に任せる）から既存のワークフローへの導入も楽だよ


## 依存管理ツールと開発用ツール

Go における依存管理ツールは現状，[**dep**](https://golang.github.io/dep/) が広く使われています．また，Go 1.11 からは Experimental ですが [**Modules**](https://github.com/golang/go/wiki/Modules) も利用可能になっています（Modules については『[Go 1.11 の modules・vgo を試す - 実際に使っていく上で考えないといけないこと](https://www.wantedly.com/companies/wantedly/post_articles/132270)』という記事で軽く感想を書いたので，よければそちらも御覧ください）．

これらの依存管理ツールはコード中で `import` するパッケージをいい感じに管理・バージョンロックをしてくれます．

### 開発用ツールの管理
一方で，開発時に利用するような実行可能ツールについては関心の外です．
よくある例としては

- [mockgen](https://github.com/golang/mock)（モック生成）
- [statik](https://github.com/rakyll/statik)（static なファイルの埋め込み）
- [sqlboiler](https://github.com/volatiletech/sqlboiler)（ORM のコード生成）
- `protoc` のプラグイン

あたりでしょうか．これらが正しくバージョン固定されないことで，いろいろな問題が発生します．

- チーム開発時に `go generate` するたびに diff がでる
- `mockgen`（コード生成ツール）と `mock`（コード中から利用するライブラリ）のバージョンがずれて動かなくなる

などなど．困る．


#### dep の `required`

一応，dep に関しては [`required` に記述することで，`import` されていないツールも管理はできます](https://golang.github.io/dep/docs/Gopkg.toml.html#required)．

```toml
required = [
  "github.com/golang/mock/mockgen",
]
```

しかしここから正しくビルドしてバイナリを生成しようとすると，もう3手間ほど必要になります:

```shell-session
# `go install` でプロジェクトローカルに出力されるように `GOBIN` を書き換え
$ export GOBIN=$PWD/bin

# `PATH` を通す
$ export PATH=$GOBIN:$PATH

# ビルドする
$ cd vendor/github.com/golang/mock/mockgen
$ go install .
```

これを毎回やるのはさすがに…．
自分は `Makefile` の `define` を利用していました．

```make
DEP_SRCS := \	
	github.com/golang/mock/mockgen \	
	golang.org/x/lint/golint	

DEP_BINS := $(addprefix $(BIN_DIR)/,$(notdir $(DEP_SRCS)))	

define dep-bin-tmpl	
$(eval OUT := $(BIN_DIR)/$(notdir $(1)))	
$(OUT): dep	
	@echo "--> Installing $(OUT)..."	
	@cd vendor/$(1) && GOBIN="$(BIN_DIR)" go install .	
endef	

$(foreach src,$(DEP_SRCS),$(eval $(call dep-bin-tmpl,$(src))))	
```

ただ，「ツールの追加時に `Gopkg.toml` と `Makefile` 両方に記述が必要になる冗長さ」と，「そもそも `Makefile` が複雑過ぎてメンテしづらい」などの問題を抱えています．


### その他の開発用ツール管理手法
この問題に対処するためのツールはいくつか存在します．

- virtualgo https://github.com/GetStream/vg
    - Python の virtualenv inspired なツール
- retool https://github.com/twitchtv/retool
    - 独自の管理機構を持つ

しかし，いずれも「vendoring したパッケージからバイナリを生成したい」程度の欲求に対し規模が大きすぎる印象を受けました．また，dep や Modules の「他ツールからの migration が容易」というメリットを殺してしまう可能性もあります．


## gex による開発用ツール管理
ここまでで「開発用ツールの管理」に欲しいものを整理すると，以下の要件を満たせれば良さそうです．

- dep や Modules に相乗りできる（独自の管理機構は持たない）
- とりあえずバイナリを簡単に生成できれば良い

上述の条件を満たすような [**gex**][gex] という薄いツールを作りました．

たとえば `mockgen` を依存に追加したければ次のコマンドを実行するだけです:

```
$ gex --add github.com/golang/mock/mockgen
```

`mockgen` を実行したいときは，`gex mockgen ...` のようにするか，add 時に `$PWD/bin` に生成されたバイナリを直接叩きます．また，`gex --build` でバイナリを再生成できます．[direnv](https://github.com/direnv/direnv) や `export PATH=$PWD/bin:$PATH` した `Makefile` との相性も良いです．


### gex がどのようにしてバイナリを管理しているか
gex は [cmd/go: clarify best practice for tool dependencies · Issue #25922 · golang/go](https://github.com/golang/go/issues/25922) で紹介されていたものを愚直に実装しています．

`gex --add <path/to/cmd>` で以下のような挙動をします．

1. `go get ...` もしくは `dep ensure -add ...` が実行される
1. `go build -o ./bin/<cmd> <path/to/cmd>` が実行される
1. `tools.go` に書き込む

`tools.go` はツールのパッケージを blank import しているだけのファイルです．build constraint に適当なことを書いているため，ビルド時には参照されないようになっています．

```go
// Code generated by github.com/izumin5210/gex. DO NOT EDIT.

// +build tools

package tools

// tool dependencies
import (
        _ "github.com/golang/mock/mockgen"
)
```

dep や Modules はこのファイルで `import` されているため，ツールのバージョンも正しく管理してくれます．


## まとめ

開発用のツール（CLI）の管理をする [gex](https://github.com/izumin5210/gex) を紹介しました．覚えるのは `gex --add` くらいで，管理自体は dep や Modules に丸投げするのでどんな環境にも気軽に導入できるはずです．

こういうのはいずれ Modules にも取り込まれるとは思いますが，それまでのつなぎに是非どうぞ．

また，gex を実際どのように使っているかなど，**Wantedly Techbook 5** で紹介しています．[**技術書典5** き20][tbf05]で頒布予定なので，そちらもよろしくおねがいします :v:


[gex]: https://github.com/izumin5210/gex
[tbf05]: https://techbookfest.org/event/tbf05/circle/53000005