2022年8月、Go 1.18対応版にアップデートしました
久しぶりのGoネタです。Go 5 Advent Calendar 2020の18日目が空いていたので書きました。
Goで実装されたコマンドでは、ビルドした時点のバージョンを埋め込むため以下のようなMakefileを用意することがあると思います。
.PHONY: build build: go build -ldflags '-X main.Version=$(VERSION)'
しかしこの方法では、go install
などMakefileを経由せずビルドしたバイナリには適切なバージョンが埋め込まれない問題があります。個人的な意見では、可能な限りgo get
でインストールできる状態を維持した方が良いと思っていますが、バージョンを埋め込むためには他に方法がないので仕方がないと理解していました。しかしGo 1.19現在、runtime/debug.ReadBuildInfoを使うと、Goモジュールで管理される場合においてはビルド時のバージョンを取れるようになっていて、これを使えば自前でバージョンを埋め込む必要がなくなるので、go install
だけで適切なバージョンを表示させることができます。
バージョンの取得
バージョンを取得したい箇所で、以下のように書きましょう。
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.19時点の定義は以下のようになっていて、mainパッケージのバージョン以外にも、コマンドが依存しているモジュールのimportパスやバージョンも取得できます。ただし、runtime/debug.BuildInfoに含まれるモジュールはコマンドをビルドするために必要なものだけです。例えばテストでのみ参照するモジュールは含まれません。
type BuildInfo struct { GoVersion string // Version of Go that produced this binary. Path string // The main package path Main Module // The module containing the main package Deps []*Module // Module dependencies Settings []BuildSetting // Other information about the build. } type Module struct { Path string // module path Version string // module version Sum string // checksum Replace *Module // replaced by this module } type BuildSetting struct { // Key and Value describe the build setting. // Key must not contain an equals sign, space, tab, or newline. // Value must not contain newlines ('\n'). Key, Value string }
BuildSettingは、ビルドしたときのフラグやリポジトリの情報を持った値です。Go 1.18時点では以下のような値を保持しています。
// Key=Valueで列挙 -compiler=gc CGO_ENABLED=1 CGO_CFLAGS= CGO_CPPFLAGS= CGO_CXXFLAGS= CGO_LDFLAGS= GOARCH=amd64 GOOS=linux GOAMD64=v1 vcs=git vcs.revision=ddf71bc3f52c7c221cea82bd26547d0346715f14 vcs.time=2020-12-17T05:50:03Z vcs.modified=true
従って、コミットハッシュもBuildInfoから参照することが可能です。
どのように動くのか
この方法を導入した場合に、バージョン表記はどうなるのかをいくつかのパターンで見ていきましょう。
普通にgo installした場合
コマンドのimportパスをそのままgo get
した場合ですね。この場合は、Goモジュールが認識する最新のバージョンが使われます。
% go install 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 install github.com/lufia/go-version-example@v0.0.1 go install 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 install 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 install github.com/lufia/go-version-example
としてコマンドをビルドした場合、このときは手元でどのようなタグ付けがされていたとしても、mainのバージョンは常に(devel)という文字列が取得されます。Makefileと併用しようとすると手元にはソースコードが一式あるはずなので、この挙動は少し厄介ですね*1。
% cd $GOPATH/src/github.com/lufia/go-version-example % go install 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以下のソースコードを眺めると良いでしょう。
他の実行ファイルに含まれるバージョンを調べたい場合
Go 1.18より、debug/buildinfoパッケージを使うと、go version -m
と同様に指定したファイルのバージョンを調べることができるようになりました。それぞれの値は上と同じなのでコードだけ紹介します。
package main import ( "debug/buildinfo" "flag" "fmt" "log" ) func main() { log.SetFlags(0) flag.Parse() for _, arg := range flag.Args() { info, err := buildinfo.ReadFile(arg) if err != nil { log.Fatalln("can't get BuildInfo:", err) } fmt.Printf("Main = %s (%s)\n", info.Main.Version, info.Main.Sum) for _, m := range info.Deps { fmt.Printf("Deps[%s] = %s (%s)\n", m.Path, m.Version, m.Sum) } fmt.Println("Settings:") for _, s := range info.Settings { fmt.Printf("\t%s=%s\n", s.Key, s.Value) } } }
まとめ
runtime/debug.ReadBuildInfoでコマンドに埋め込まれたバージョンを取得できることを紹介しました。多くの場合は、これを使うとgo install
だけでも適切なバージョンが取得できると思います。個人的にはgo install
でコマンドを導入することが多くあるので、Makefileに依存しないこの方法が広まってくれると嬉しいのですが、Makefileでクロスコンパイルしている場合は容易に(devel)表記となってしまうところは、少し注意が必要かもしれませんね。