Carpe Diem

備忘録

go modulesでコマンドラインツールのバージョン管理をする

概要

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にも載っているくらいなので現状このやり方が推奨されているかと思います。

ソース