久しぶりのGoネタです。Go 5 Advent Calendar 2020の18日目が空いていたので書きました。
Goで実装されたコマンドでは、ビルドした時点のバージョンを埋め込むため以下のようなMakefileを用意することがあると思います。
.PHONY: build build: go build -ldflags '-X main.Version=$(VERSION)'
しかしこの方法では、go get
などMakefileを経由せずビルドしたバイナリには適切なバージョンが埋め込まれない問題があります。個人的な意見では、可能な限りgo get
でインストールできる状態を維持した方が良いと思っていますが、バージョンを埋め込むためには他に方法がないので仕方がないと理解していました。しかしGo 1.15現在、runtime/debug.ReadBuildInfoを使うと、Goモジュールで管理される場合においてはビルド時のバージョンを取れるようになっていて、これを使えば自前でバージョンを埋め込む必要がなくなるので、go get
だけで適切なバージョンを表示させることができます。
ところで、Go1.16からの go get と go install についてで書かれているように、Go 1.16からgo get
の挙動が変わってしまいます。この記事を書いている時点ではまだGo 1.15なのでgo get
表記を使っていますが、おそらく記事中のgo get
をgo install
に読み替えると、1.16以降もそのまま使えるのではないかと思います。
バージョンの取得
バージョンを取得したい箇所で、以下のように書きましょう。
import ( "runtime/debug" ) func Version() string { info, ok := debug.ReadBuildInfo() if !ok { // Goモジュールが無効など return "(devel)" } return info.Main.Version }
これで、v1.0.0などのような、Goモジュールが認識しているバージョン文字列を取得できます。簡単ですね。
runtime/debug.BuildInfo構造体
Go 1.15時点の定義は以下のようになっていて、mainパッケージのバージョン以外にも、コマンドが依存しているモジュールのimportパスやバージョンも取得できます。ただし、runtime/debug.BuildInfoに含まれるモジュールはコマンドをビルドするために必要なものだけです。例えばテストでのみ参照するモジュールは含まれません。
type BuildInfo struct { Path string // The main package path Main Module // The module containing the main package Deps []*Module // Module dependencies } type Module struct { Path string // module path Version string // module version Sum string // checksum Replace *Module // replaced by this module }
どのように動くのか
この方法を導入した場合に、バージョン表記はどうなるのかをいくつかのパターンで見ていきましょう。
普通にgo getした場合
コマンドのimportパスをそのままgo get
した場合ですね。この場合は、Goモジュールが認識する最新のバージョンが使われます。
% go get github.com/lufia/go-version-example go: downloading github.com/lufia/go-version-example v0.0.2 go: github.com/lufia/go-version-example upgrade => v0.0.2 go: downloading github.com/pkg/errors v0.9.1 % go-version-example Main = v0.0.2 Deps[github.com/pkg/errors] = v0.9.1
古いバージョンを指定した場合
この場合も、正しくバージョンを取得できます。
% go get github.com/lufia/go-version-example@v0.0.1 go get github.com/lufia/go-version-example@v0.0.1 go: downloading github.com/lufia/go-version-example v0.0.1 % go-version-example Main = v0.0.1
ブランチ名などを明記した場合
正規のバージョンよりmainブランチが進んでいる場合に、@mainを明記してgo get
した場合はバージョンの後ろに時刻とハッシュ値が付与されます。
% go get github.com/lufia/go-version-example@main go: downloading github.com/lufia/go-version-example v0.0.2-0.20201217055003-ddf71bc3f52c go: github.com/lufia/go-version-example main => v0.0.2-0.20201217055003-ddf71bc3f52c % go-version-example Main = v0.0.2-0.20201217055003-ddf71bc3f52c Deps[github.com/pkg/errors] = v0.9.1
カレントディレクトリがmainモジュールと同じ場合
少し分かりづらいのですが、カレントディレクトリがGoモジュール管理下にあり、そのimportパスとgo get
するimportパスが同一の場合。例えば、
// go.mod module github.com/lufia/go-version-example
を持つリポジトリの中で
% go get github.com/lufia/go-version-example
としてコマンドをビルドした場合、このときは手元でどのようなタグ付けがされていたとしても、mainのバージョンは常に(devel)という文字列が取得されます。Makefileと併用しようとすると手元にはソースコードが一式あるはずなので、この挙動は少し厄介ですね*1。
% cd $GOPATH/src/github.com/lufia/go-version-example % go get github.com/lufia/go-version-example % go-version-example Main = (devel) Deps[github.com/pkg/errors] = v0.9.1
どうやっているのか
2020-12-18 16:30: 対応フォーマットの話はgo version -m
の話だったので書き換えました。
runtime/debug.ReadBuildInfoは何を読んでいるのか、ですが、ビルド時にruntime.modinfoへモジュール情報が埋め込まれていて、これをruntime/debug.BuildInfoへパースしているようですね。雰囲気で読んだ感じだと、埋め込んでいるのはgo/src/cmd/go/internal/work/exec.goのbuild()辺りかな。
runtime.modinfoとだいたい同じ値がgo version -m
で調べることができます。こっちはGo 1.13以降で対応されました。
% go version -m ~/bin/ivy /Users/lufia/bin/ivy: go1.14.6 path robpike.io/ivy mod robpike.io/ivy v0.0.0-20191204195242-5feaa23cbcf3 h1:tff9UcwX5PKD7Z+Q7O9EIILnQVm5uRY0/xP90+oXtog=
go version
が読む情報は、例えばELFフォーマットのバイナリでは.go.buildinfo
という名前のセクションに64KBのデータとして存在します。Mach-Oの場合はセクション名が__go_buildinfo
であったりなど若干の差はありますが、どれも同じデータが埋め込まれています。これらの詳細は、go/src/cmd/go/internal/version以下のソースコードを眺めると良いでしょう。
まとめ
runtime/debug.ReadBuildInfoでコマンドに埋め込まれたバージョンを取得できることを紹介しました。多くの場合は、これを使うとgo get
だけでも適切なバージョンが取得できると思います。個人的にはgo get
でコマンドを導入することが多くあるので、Makefileに依存しないこの方法が広まってくれると嬉しいのですが、Makefileでクロスコンパイルしている場合は容易に(devel)表記となってしまうところは、少し注意が必要かもしれませんね。