Go 1.18 で追加される debug/buildinfo

Go 1.18 で新しく追加される [`debug/buildinfo`](https://pkg.go.dev/debug/buildinfo@go1.18beta1) パッケージについて調べました。

※ 特に記載がない限り 2022 年 2 月 20 日時点 (Go 1.18 rc.1) の情報になります。

## 概要

https://pkg.go.dev/debug/buildinfo@go1.18rc1

従来からビルドに使用された Go のバージョンや依存パッケージの情報はバイナリに含まれており `go version -m` で出力できましたが内部パッケージとして実装されているため外部からは利用できませんでした。 [^1] [^2]
これらの機能を外部から利用できるようにしたのが `debug/buildinfo` です。

https://github.com/golang/go/issues/39301

[^1]: https://github.com/rsc/goversion
[^2]: https://knqyf263.hatenablog.com/entry/2021/02/12/162928

また、 Go 1.18 ではビルドしたソースコードのバージョン管理情報やビルドオプションもバイナリに追加されるようになりました。
これによって従来は ldflag などで埋め込んでいた git の commit ハッシュなどを runtime から取得できるようになります。

https://github.com/golang/go/issues/37475

## 取得できる情報

Go 1.18 では `runtime/debug.ReadBuildInfo()` で取得できる `debug.BuildInfo` の情報が増えました。
また、 `debug/buildinfo` パッケージを使うことで自分自身だけではなく、バイナリファイルまたは `io.Reader` から `debug.BuildInfo` を取得できるようになりました。

| 項目名      | 型               | 内容                                                 |
| ----------- | ---------------- | ---------------------------------------------------- |
| `GoVersion` | `string`         | ビルドに使用された Go のバージョン文字列             |
| `Path`      | `string`         | main パッケージパス (既存項目)                       |
| `Main`      | `Module`         | main パッケージが含まれるモジュールの情報 (既存項目) |
| `Deps`      | `[]*Module`      | 依存パッケージのモジュール情報 (既存項目)            |
| `Settings`  | `[]BuildSetting` | ビルドに関する情報 (Key - Value 形式)                |

Go 1.18 で追加されたのは `GoVersion` と `Settings` で他の項目に変更はありません。

手元の環境だと `Settings` には次の情報が出力されました。

| Key            | Value     | 内容                                                                   |
| -------------- | --------- | ---------------------------------------------------------------------- |
| `-compiler`    | `gc`      | ビルドに使用されたコンパイラ (`gc` とか `gccgo`)                       |
| `CGO_ENABLED`  | `1`       | ビルドに使用される環境変数                                             |
| `CGO_CFLAGS`   |           | ビルドに使用される環境変数                                             |
| `CGO_CPPFLAGS` |           | ビルドに使用される環境変数                                             |
| `CGO_CXXFLAGS` |           | ビルドに使用される環境変数                                             |
| `CGO_LDFLAGS`  |           | ビルドに使用される環境変数                                             |
| `GOARCH`       | `amd64`   | ビルドターゲットの CPU アーキテクチャ                                  |
| `GOOS`         | `windows` | ビルドターゲットの OS 名                                               |
| `GOAMD64`      | `v1`      | ビルドターゲット `amd64` のマイクロアーキテクチャレベル                |
| `vcs`          | `git`     | バージョン管理システムの名前 (`git`, `hg`, `bzr`, `fossil` をサポート) |
| `vcs.revision` | (略)      | ソースコードのリビジョンを示す情報 (git の場合は commit ハッシュ)      |
| `vcs.time`     | (略)      | ソースコードのリビジョン更新時刻 (git の場合は commit 時刻)            |
| `vcs.modified` | `true`    | commit されていない変更があるかどうか                                  |

`GOAMD64` は Go 1.18 で追加される環境変数で `amd64` のマイクロアーキテクチャレベルを指定します。 [^3]
デフォルトは `v1` で `v2`, `v3`, `v4` と世代が上がると利用できる命令が増えてビルドされるバイナリが最適化されます。

[^3]: https://github.com/golang/go/issues/45453

git tag のような情報はバージョンがあいまいになり予測できない動作を引き起こすという理由で含まれていません。 [^4]
個人的に入れてほしい情報ですが commit ハッシュと違って tag は付け替えもできるし複数設定もできるので確かにその通りではあります。

[^4]: https://github.com/golang/go/issues/37475#issuecomment-607452893

~~また、バージョン管理システムの情報を含めないオプションを用意するのことでしたが見つけられませんでした。~~ [^5]

[^5]: https://github.com/golang/go/issues/37475#issuecomment-880791126

また、ビルド時に `-buildvcs=false` を指定することでバージョン管理システムの情報をバイナリに含めないようにできます。
Go 1.18 beta.1 までは `-buildinfo=false` を指定することでビルドオプションもバイナリに含めないようにできましたがこのオプションは Go 1.18 beta.2 で廃止されました。 [^6]

[^6]: https://github.com/golang/go/commit/d54f6630703900948d9757487a1bffd87b7d36e2

https://tip.golang.org/doc/go1.18#go-command

ちなみに `Main.Version` には `(devel)` という文字列が設定されます。
従来からの動作ですが少し残念な感じです。

https://github.com/golang/go/issues/29228

## go version コマンドで出力する

従来の `go version -m` は引き続き利用できます。
Go 1.18 でビルドしたバイナリだと表示される情報が増えますが、 Go 1.18 でビルドしたバイナリは Go 1.17 で表示できないようです。

Go 1.17 で `go version -m` を使った場合の出力例

```console
$ go version -m testdata/hello.go117.exe
testdata/hello.go117.exe: go1.17.5
        path    github.com/sg0hsmt/try-goapp/hello
        mod     github.com/sg0hsmt/try-goapp    (devel)
        dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
        dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
```

```console
$ go version -m testdata/hello.go118.exe
testdata/hello.go118.exe: go version not found
```

Go 1.18 rc.1 で `go version -m` を使った場合の出力例

```console
$ go1.18rc1 version -m testdata/hello.go117.exe
testdata/hello.go117.exe: go1.17.5
        path    github.com/sg0hsmt/try-goapp/hello
        mod     github.com/sg0hsmt/try-goapp    (devel)
        dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
        dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
```

```console
$ go1.18rc1 version -m testdata/hello.go118.exe
testdata/hello.go118.exe: go1.18beta1
        path    github.com/sg0hsmt/try-goapp/hello
        mod     github.com/sg0hsmt/try-goapp    (devel)
        dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
        dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
        build   -compiler=gc
        build   CGO_ENABLED=1
        build   CGO_CFLAGS=
        build   CGO_CPPFLAGS=
        build   CGO_CXXFLAGS=
        build   CGO_LDFLAGS=
        build   GOARCH=amd64
        build   GOOS=windows
        build   GOAMD64=v1
        build   vcs=git
        build   vcs.revision=619f4a18485903816d91d53a6d1618d5024d47d1
        build   vcs.time=2021-12-19T07:51:57Z
        build   vcs.modified=true
```

Go 1.18 でもビルド時に `-buildvcs=false` オプションを指定することでバージョン管理システムの情報が含まれないように制限できます。
~~`-buildvcs=false` と `-buildinfo=false` を両方指定するとバイナリに含まれる情報は Go 1.17 と同等になります。~~

```console
$ go1.18rc1 build -buildvcs=false -o hello.go118.exe .

$ go1.18rc1 version -m hello.go118.exe
hello.go118.exe: go1.18rc1
        path    github.com/sg0hsmt/try-goapp/hello
        mod     github.com/sg0hsmt/try-goapp    (devel)
        dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
        dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
        build   -compiler=gc
        build   CGO_ENABLED=1
        build   CGO_CFLAGS=
        build   CGO_CPPFLAGS=
        build   CGO_CXXFLAGS=
        build   CGO_LDFLAGS=
        build   GOARCH=amd64
        build   GOOS=windows
        build   GOAMD64=v1
```

なお Go 1.18 rc.1 時点では `go version` の対象が実行ファイルに制限されており Windows で Linux 向けバイナリを表示させようとすると失敗します。 [^7]

```console
$ go1.18rc1 version -m testdata/hello.go118.linux
testdata/hello.go118.linux: not executable file
```

[^7]: https://github.com/golang/go/issues/48187

## debug/buildinfo パッケージを使う

`debug/buildinfo.ReadFile()` にファイルパスを渡すと `*debug.BuildInfo` を返してくれるので利用は簡単です。
大雑把に次のようなコードで出力できます。
(Go 1.18 rc.1 で `String()` が実装され `MarshalText()` が廃止されました [^8])

[^8]: https://github.com/golang/go/commit/9cec77ac11b012283e654b423cf85cf9976bedd9

```go
func main() {
	var path string

	flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
	flags.StringVar(&path, "path", "", "path of target binary")
	flags.Parse(os.Args[1:])

	info, err := buildinfo.ReadFile(path)
	if err != nil {
		log.Fatalln("can not read build info from file:", err)
	}

	fmt.Println(info)
}
```

```console
$ go1.18rc1 run main.go -path testdata/hello.go117.exe
go      go1.17.5
path    github.com/sg0hsmt/try-goapp/hello
mod     github.com/sg0hsmt/try-goapp    (devel)
dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
```

```console
$ go1.18rc1 run main.go -path testdata/hello.go118.exe
go      go1.18beta1
path    github.com/sg0hsmt/try-goapp/hello
mod     github.com/sg0hsmt/try-goapp    (devel)
dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
build   -compiler=gc
build   CGO_ENABLED=1
build   CGO_CFLAGS=
build   CGO_CPPFLAGS=
build   CGO_CXXFLAGS=
build   CGO_LDFLAGS=
build   GOARCH=amd64
build   GOOS=windows
build   GOAMD64=v1
build   vcs=git
build   vcs.revision=619f4a18485903816d91d53a6d1618d5024d47d1
build   vcs.time=2021-12-19T07:51:57Z
build   vcs.modified=true
```

自分自身の情報を取得したい場合は従来通り `debug.ReadBuildInfo()` を呼べば `*debug.BuildInfo` を取得できます。

また `debug/buildinfo` パッケージを使用した場合は Windows でも Linux 向けバイナリの表示が可能です。
(さすがに wasm はだめでした)

```console
$ go1.18rc1 run main.go -path testdata/hello.go118.linux
go      go1.18beta1
path    github.com/sg0hsmt/try-goapp/hello
mod     github.com/sg0hsmt/try-goapp    (devel)
dep     github.com/google/uuid  v1.3.0  h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
dep     github.com/maxence-charriere/go-app/v9  v9.2.1  h1:wP+A7zMDc1DIxXvhZQvWVq+vzlb8Hmo75AEVMChoVRg=
build   -compiler=gc
build   CGO_ENABLED=0
build   GOARCH=amd64
build   GOOS=linux
build   GOAMD64=v1
build   vcs=git
build   vcs.revision=619f4a18485903816d91d53a6d1618d5024d47d1
build   vcs.time=2021-12-19T07:51:57Z
build   vcs.modified=true
```

## ユースケース

主なユースケースとして次の使い方が想定されているようです。
Go で実装されたアプリケーションを運用していくうえで重要な機能になりそうです。

コンテナ内のバイナリに対して脆弱性なパッケージが組み込まれていないかチェックする。

https://github.com/golang/go/issues/39301#issuecomment-635984312

SBOM (ソフトウェア部品表) を作成する。

https://github.com/goreleaser/goreleaser/issues/2597