概要
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
といった感じにしてください。
その他
1.16からgo installでバージョン固定できるよ
go install now accepts arguments with version suffixes (for example, go install example.com/cmd@v1.0.0). This causes go install to build and install packages in module-aware mode, ignoring the go.mod file in the current directory or any parent directory, if there is one. This is useful for installing executables without affecting the dependencies of the main module.
ref: https://go.dev/doc/go1.16#go-command
とあるようにバージョン固定ができるようになりました。これによりtools.goにこだわらなくても、バイナリバージョンを固定することができます。
しかしながらgo.modをイジらないということはdependabotのようなバージョン更新の自動化botも検知が難しくなるので、やはりgo.modを利用するやり方の方がバージョン管理を1つに集約できる&周辺ツールのエコシステムに乗っかれて便利かと思います。
1.17からgo runでバージョン固定できるよ
go run now accepts arguments with version suffixes (for example, go run example.com/cmd@v1.0.0). This causes go run to build and run packages in module-aware mode, ignoring the go.mod file in the current directory or any parent directory, if there is one. This is useful for running executables without installing them or without changing dependencies of the current module.
ref: https://go.dev/doc/go1.17#go-command
さらに1.17からはgo run
でもバージョン固定ができるようになりました。
これにより例えばgo generate
でも以下のようにバージョンを指定して実行することができるので、tools.goが無くても固定が可能になります。
//go:generate go run golang.org/x/tools/cmd/stringer@latest
しかしながらこの使い方は
- バージョンが各ファイルに分散されてしまうこと
- 1.16のgo installでも話したエコシステムの恩恵を受けられなくなること
といった課題があります。
一方CIなどで特定のgoツールを利用したい、といったユースケースではとても適していると思います。
まとめ
go modulesでコマンドラインツールのバージョン固定方法を紹介しました。
Wikiにも載っているくらいなので現状このやり方が推奨されているかと思います。