TL;DR

go generate で使うものや linter などの開発用ツールもバージョン固定したいよね

gex を使えば簡単にできるよ
独自の管理機構を持たない(dep / Modules に任せる)から既存のワークフローへの導入も楽だよ

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

Go における依存管理ツールは現状,dep が広く使われています.また,Go 1.11 からは Experimental ですが Modules も利用可能になっています(Modules については『Go 1.11 の modules・vgo を試す – 実際に使っていく上で考えないといけないこと』という記事で軽く感想を書いたので,よければそちらも御覧ください).

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

開発用ツールの管理

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

mockgen(モック生成)

statik(static なファイルの埋め込み)

sqlboiler(ORM のコード生成)

protoc のプラグイン

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

    • チーム開発時に go generate するたびに diff がでる

mockgen(コード生成ツール)と mock(コード中から利用するライブラリ)のバージョンがずれて動かなくなる

などなど.困る.

dep の required

一応,dep に関しては required に記述することで,import されていないツールも管理はできます.

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

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

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

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

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

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

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 という薄いツールを作りました.

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

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

mockgen を実行したいときは,gex mockgen … のようにするか,add 時に $PWD/bin に生成されたバイナリを直接叩きます.また,gex –build でバイナリを再生成できます.direnv や export PATH=$PWD/bin:$PATH した Makefile との相性も良いです.

gex がどのようにしてバイナリを管理しているか

gex は cmd/go: clarify best practice for tool dependencies · Issue #25922 · golang/go で紹介されていたものを愚直に実装しています.

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

go get … もしくは dep ensure -add … が実行される

go build -o ./bin/ <path/to/cmd> が実行される

tools.go に書き込む

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

// 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 を紹介しました.覚えるのは gex –add くらいで,管理自体は dep や Modules に丸投げするのでどんな環境にも気軽に導入できるはずです.

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

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