概要
goはModulesでリポジトリのライブラリのバージョン管理を行えます。
ただコマンドラインツールに関してはgo get
してgo.mod
に追加されても、goファイルで扱っているわけではないのでgo mod tidy
すると消えてしまいます。
しかしながら「この機能は最新のmasterにしかない。かと言ってgo get -u
や@master
で常に最新にしてしまうと互換性が壊れるかもしれない。だからバージョン管理できないと困る」というシチュエーションはよくあります。
その対策としては現状tools.go
にblank importするのがベストプラクティスとなっています。
環境
- go 1.13.6
現状の問題
入ったはずのパッケージがgo mod tidy
で消える
$ mkdir hello $ cd hello $ go mod init example.com/hello
としてから、stringer
を使いたいと考えインストールしてみます。
$ go install golang.org/x/tools/cmd/stringer
するとgo.mod
にそのライブラリ及びインストールしたバージョンが追加されます。
$ cat go.mod module example.com/hello go 1.13 require golang.org/x/tools v0.0.0-20200110213125-a7a6caa82ab2 // indirect
しかしこれは直接goファイルで扱っているわけではないので、go mod tidy
すると消えてしましいます。
$ go mod tidy $ cat go.mod module example.com/hello go 1.13
blank importでgo.modに残す
そこでtools.go
といった適当なファイルを用意し、_
importを使います。
// +build tools package tools import ( _ "golang.org/x/tools/cmd/stringer" )
こうするとgo mod tidy
してもgo.mod
に残り続けます。
$ go install golang.org/x/tools/cmd/stringer $ go mod tidy $ cat go.mod module example.com/hello go 1.13 require golang.org/x/tools v0.0.0-20200110213125-a7a6caa82ab2
// +build tools
を付けるのはなぜか?
単純に
package tools import ( _ "golang.org/x/tools/cmd/stringer" )
としてしまうと、通常のビルド時やgo get
時に
tools.go:6:2: import "golang.org/x/tools/cmd/stringer" is a program, not an importable package
と怒られてしまいます。
なので通常のビルドに含まれないように付けます。
インストールする時は?
go installを使う
go get
はライブラリが更新されていると勝手にバージョンが上がるので基本的にgo install
を使います。
go install
であればgo.mod
に書かれたバージョンをインストールしてくれるのでコマンドラインツールのバージョン固定が可能です。
複数のCLIをインストールしたい
import ( _ "github.com/golang/mock/mockgen" _ "golang.org/x/tools/cmd/stringer" )
こんな感じで複数のコマンドラインツールがある場合は
$ cat tools.go | awk -F'"' '/_/ {print $2}' | xargs -tI {} go install {}
ちょっと泥臭いですがこのようにシェルコマンドで対応します。
Makefileに書くときは$2
のところを$$2
にするよう注意してください。
install-go-tools: cat tools.go | awk -F'"' '/_/ {print $$2}' | xargs -tI {} go install {}
リポジトリ毎にバイナリを管理したい
以上でインストールするコマンドラインツールのバージョンを固定できるようになりましたが、他のところで別のバージョンをインストールして上書きしてしまった場合、もう一度インストールする必要が出てきます。
そういった手間を省くために環境変数GOBIN
でインストールする場所を指定し、そこにPATH
を通して実行するバイナリの場所も指定します。
例えば先程のMakefileを↓のように変更します。
BIN := $(abspath .bin) install-go-tools: cat tools.go | awk -F'"' '/_/ {print $$2}' | GOBIN=$(BIN) xargs -tI {} go install {} mockgen: PATH=$(BIN):$(PATH) mockgen hoge.go
ポイント
ポイントとしては以下です。
インストール時にGOBIN
を指定
インストールしたいところにGOBIN
を指定します。
この例ではリポジトリルート/.bin
にしています。
xargs -tI {} go install {} ↓ BIN := $(abspath .bin) GOBIN=$(BIN) xargs -tI {} go install {}
実行時は一時的にPATH
に追加
GOBIN
はインストール場所なだけで、そのコマンドラインツールを実行する時にセットしても
# グローバルは最新 $ mockgen -version v1.6.0 # リポジトリ用に古いバージョンを入れてみる $ GOBIN=./.bin go install github.com/golang/mock/mockgen@v1.5.0 # GOBINをつけて実行しても使うのはグローバル $ GOBIN=./.bin mockgen -version v1.6.0
このようにインストールしたバイナリを使ってくれないです。
go generate
のようなコマンドを使っても同様です。
PATH
の先頭に置く
Goを使う人のほとんどが
export PATH=$PATH:$GOPATH/bin
してると思いますが、このように後ろにつけてしまう(PATH=$(PATH):$(BIN)
)と$GOPATH/bin
が優先されてしまい$BIN
が使われません。なので
PATH=$(BIN):$(PATH)
としましょう。
abspathで相対パスでなく絶対パスに
シェルではBIN := $(abspath .bin)
としてます。
これはPATH
に相対パスを設定すると実行場所が変わった時に実行できなくなるためです。
なのでMakefileでなくターミナルで直接使う場合はGOPATH=$(PWD)/.bin
といった感じにしてください。
まとめ
go modulesでコマンドラインツールのバージョン固定方法を紹介しました。
Wikiにも載っているくらいなので現状このやり方が推奨されているかと思います。