🗃️

Go製CUIツールのバージョン表示に便利なパッケージを作りました

2022/02/26に公開

どういったものを作ったのか

今回作ったのは、curverというgoのパッケージです。
https://github.com/harakeishi/curver
curverは、goで作成されたCUIツールのバージョンの表示を簡単にしてくれるものです。
使い方は簡単で、以下のようにgo getしてimportすれば準備OKです。

shell
$ go get github.com/harakeishi/curver@latest
main.go
package main

import (
	"github.com/harakeishi/curver"
)

func main () {
    curver.EchoVersion()
}

これで、以下のようにビルドするとEchoVersion()で指定したバージョンが表示されます。

shell
$ go build -ldflags '-X github.com/harakeishi/curver.Version=v0.1.0' -o ./main
$ ./main
version: v0.1.0

git tagをバージョンとして使用したければ以下のようにすれば、その時点で最新のtagがVersionとして使われます。

shell
$ go build -ldflags "-X github.com/harakeishi/curver.Version=$(git describe --tags)" -o ./main
$ ./main
version: v0.1.0

goreleaserなどを使っている場合は、以下のように設定すればOKです。

.goreleaser.yaml
builds:
  - eldflags:
      - -s -w -X github.com/harakeishi/curver.Version={{.Version}}

どうしてつくったのか

こんなパッケージ使わなくても普通にmain内で宣言した変数をldflagsで書き換えればいいじゃんと思った方もいるかと思います。
もちろんそれでもいいのですが、goreleaserを使ってGithub Actionsでリリースをするような場合などは意図したとおりに動かないと思います。
この方法をとっている場合、Github ReleasesのAssetsからバイナリを落としてきて使うと問題なくバージョンがldflagsで置換えたものが表示されると思います。
ですが、go installを使用してインストールされたものはldflagsで設定したように置き換えられていないと思います。
これは(たぶんですが)go installする際、Actionsで生成したバイナリをDLしてくるのではなく、ソースを手元に落としてきて手元でビルドしているためldflagsが使われずビルドされてしまうのではないかと思います。

なにをしてるか

では、go installされた場合のバージョン表示ってどうすればいいのでしょうか?
下記のようにruntime/debugパッケージを使えば良さそうです。

main.go
package main

import (
	"fmt"
	"runtime/debug"
)

func main () {
    if buildInfo, ok := debug.ReadBuildInfo(); ok {
	fmt.Println(buildInfo.Main.Version)
    }
}

debug.ReadBuildInfo()は、下記に書いてあるようにビルド時のデバック情報を返してくれるようです。

ReadBuildInfo returns the build information embedded in the running binary. The information is available only in binaries built with module support.

これすごく便利なのですが、カレントディレクトリがGoモジュール管理下でビルドする場合、初期値である(devel)という値が入ってしまうみたいです。
つまり、Github Actionでビルドする際は(devel)という値が入ってしまい、生成したバイナリでバージョンを確認すると(devel)が表示されてしまうわけです。

ここまででわかったことは

  • ldflagsを使用する場合go installするとバージョン表示が意図したとおりにならない
  • debug.ReadBuildInfo()を使用する場合、Github Actionを通し生成したバイナリはバージョン表示が意図したとおりにならない

これらを踏まえてどちらにも対応したい場合以下のようにする必要があります。

main.go
package main

import (
	"fmt"
	"runtime/debug"
)

var version string
func main () {
    if version != "" {
    // ldflagsでバージョンを埋め込めている場合はそれを表示
        fmt.Println(version)
    }
    if buildInfo, ok := debug.ReadBuildInfo(); ok {
    // ldflagsでバージョンを埋め込めていない場合(go installされた場合)
    // debug.ReadBuildInfo()を使ってバージョンを表示
	fmt.Println(buildInfo.Main.Version)
    }
}

これでやっと、両方共バージョンを表示できるようになりました。
しかし、goでCUIツールを作るたびバージョンを表示するためだけにこの処理を書くのはめんどくさいですよね。
ということで、1行で上記の処理をできるようにパッケージ化してしまえばと思い今回パッケージ化しました。

もし、バージョンだけほしいんじゃ!!って場合は以下のようにするとバージョンを文字列で取得できます。

main.go
package main

import (
	"github.com/harakeishi/curver"
)

func main () {
    version := curver.GetVersion()
}

おわりに

今回、自作のコマンドにバージョン表示機能をつけようと思ったけどなかなかうまく行かず手こずったのでもっと簡単にできるようにしたいという思いからパッケージを作成しました。
もし、Goで作ったもののバージョン表示に困ったら使ってみてもらえると嬉しいです。
curverへのPR・issueも気軽にお待ちしています!!

おまけ

curverは、current versionを略して命名しました。

参考

Discussion