Report the Go build info version when linker flags set none (closes #422)
check / check (push) Successful in 12m33s
check / check (push) Successful in 12m33s
A `go install` of a tag sets no linker flags, so the binary reported `dev`. Version, commit and commit date that the linker flags leave unset now come from the build info Go records. Go records a pseudo-version for an untagged commit and a `+dirty` suffix for a modified checkout; `IsDevVersion` now counts both as development builds, using `golang.org/x/mod`. Model: opus-5-5
This commit is contained in:
1 parent
aae7fa17ae
commit
e82aa12f27
5 files changed
+193
-18
No files matched your search
@@ -21,6 +21,8 @@ const shortCommitLen = 12
|
||||
const exitCodeInterrupted = 130
|
||||
|
||||
// Entry is the main entry point for the CLI application.
|
||||
// It first takes the version, commit and commit date the linker flags
|
||||
// left unset from the Go build info (see globals.FromBuildInfo).
|
||||
// It prints the startup banner to stderr (unless a banner-suppressing
|
||||
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
||||
// root cobra command, and routes any returned error through the
|
||||
@@ -33,6 +35,7 @@ const exitCodeInterrupted = 130
|
||||
// deferred profile writers run before the process ends. See run in
|
||||
// cmd/vaultik/main.go.
|
||||
func Entry() int {
|
||||
globals.UseBuildInfo()
|
||||
emitStartupBanner(os.Args[1:], os.Stderr)
|
||||
|
||||
rootCmd := NewRootCommand()
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
// Package globals holds application-wide metadata (name, version,
|
||||
// commit) that is populated at build time via linker flags.
|
||||
// commit) that is populated at build time via linker flags, or from the
|
||||
// build info Go records in the binary when the linker flags set none.
|
||||
package globals
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
)
|
||||
|
||||
// Appname is the application name, populated from main().
|
||||
var Appname = "vaultik" //nolint:gochecknoglobals // set via -ldflags at build time
|
||||
|
||||
// DevVersion is the version a binary reports when it was built without
|
||||
// git metadata: script/version emits it outside a git checkout, and an
|
||||
// unstamped `go build` keeps it. goreleaser's snapshot template stamps
|
||||
// git metadata: script/version emits it outside a git checkout, and a
|
||||
// binary keeps it when neither its linker flags nor its Go build info
|
||||
// name a version. goreleaser's snapshot template stamps
|
||||
// it followed by "-" and the commit it was built from. It is
|
||||
// deliberately not a number.
|
||||
const DevVersion = "dev"
|
||||
@@ -32,6 +37,52 @@ var Commit = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
||||
// CommitDate is the ISO-8601 date of the commit, populated from main().
|
||||
var CommitDate = Unknown //nolint:gochecknoglobals // set via -ldflags at build time
|
||||
|
||||
// develVersion is the main module version Go records when it cannot
|
||||
// tell which version it built.
|
||||
const develVersion = "(devel)"
|
||||
|
||||
// UseBuildInfo applies FromBuildInfo to Version, Commit and CommitDate,
|
||||
// with the build info of the running binary.
|
||||
func UseBuildInfo() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
Version, Commit, CommitDate = FromBuildInfo(
|
||||
info, Version, Commit, CommitDate)
|
||||
}
|
||||
|
||||
// FromBuildInfo returns version, commit and commitDate, with each one
|
||||
// the linker flags left unset taken from info. A `go install` of a tag
|
||||
// sets no linker flags, but Go records the tag as the main module's
|
||||
// version. Go records the commit and its time only when it builds from
|
||||
// a git checkout. The time is cut to a date, as the Makefile stamps it.
|
||||
func FromBuildInfo(
|
||||
info *debug.BuildInfo, version, commit, commitDate string,
|
||||
) (string, string, string) {
|
||||
if (version == "" || version == DevVersion) &&
|
||||
info.Main.Version != "" && info.Main.Version != develVersion {
|
||||
version = info.Main.Version
|
||||
}
|
||||
|
||||
for _, s := range info.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
if commit == Unknown {
|
||||
commit = s.Value
|
||||
}
|
||||
case "vcs.time":
|
||||
t, err := time.Parse(time.RFC3339, s.Value)
|
||||
if commitDate == Unknown && err == nil {
|
||||
commitDate = t.UTC().Format(time.DateOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return version, commit, commitDate
|
||||
}
|
||||
|
||||
// Author identifies the upstream author of vaultik.
|
||||
const Author = "Jeffrey Paul <sneak@sneak.berlin>"
|
||||
|
||||
@@ -68,8 +119,10 @@ func New() (*Globals, error) {
|
||||
// a release. "dev" and goreleaser's snapshot "dev-<sha>" count, and so
|
||||
// does what `git describe --tags --always --dirty` gives a make or
|
||||
// docker build of an untagged commit: the bare short commit, or
|
||||
// tag-N-gHASH on a commit after a tag. Any version ending in "-dirty"
|
||||
// counts, a modified checkout of a tag ("v1.0.0-dirty") included.
|
||||
// tag-N-gHASH on a commit after a tag. A Go pseudo-version counts: Go
|
||||
// records one for a build of an untagged commit without linker flags.
|
||||
// Any version ending in "-dirty" or "+dirty" counts, a modified
|
||||
// checkout of a tag ("v1.0.0-dirty", "v1.0.0+dirty") included.
|
||||
// A plain tag such as "v1.0.0" or "1.0.0" is a release.
|
||||
//
|
||||
// The empty string counts too. Nothing that knows its version reports
|
||||
@@ -82,7 +135,8 @@ func New() (*Globals, error) {
|
||||
func IsDevVersion(v string) bool {
|
||||
if v == "" || v == Unknown || v == DevVersion ||
|
||||
strings.HasPrefix(v, DevVersion+"-") ||
|
||||
strings.HasSuffix(v, "-dirty") {
|
||||
strings.HasSuffix(v, "-dirty") || strings.HasSuffix(v, "+dirty") ||
|
||||
module.IsPseudoVersion(v) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package globals_test
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/vaultik/internal/globals"
|
||||
@@ -59,12 +60,20 @@ func TestIsDevVersion(t *testing.T) {
|
||||
{"1.0.0-rc.1-12-g877eb2f", true},
|
||||
// A tagged commit with uncommitted changes is not that tag.
|
||||
{"v1.0.0-dirty", true},
|
||||
// What Go records for a build without linker flags: a
|
||||
// pseudo-version for an untagged commit, "+dirty" for a
|
||||
// modified checkout.
|
||||
{"v1.0.0-rc3.0.20261111001226-abcdefabcdef", true},
|
||||
{"v0.0.0-20261111001226-abcdefabcdef", true},
|
||||
{"v1.0.1-0.20261111001226-abcdefabcdef", true},
|
||||
{"v1.0.0+dirty", true},
|
||||
// What a tagged build produces (goreleaser's .Version strips
|
||||
// the leading "v"; script/version keeps it).
|
||||
{"1.0.0", false},
|
||||
{"0.1.0", false},
|
||||
{"1.0.0-rc.1", false},
|
||||
{"v1.0.0", false},
|
||||
{"v1.0.0-rc3", false},
|
||||
// A release must not be mistaken for a dev build just because
|
||||
// the string happens to contain "dev".
|
||||
{"1.0.0-dev", false},
|
||||
@@ -86,10 +95,9 @@ func TestIsDevVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultVersionIsDev pins the linker-flag contract: an unstamped
|
||||
// binary (no -ldflags at all, which is what `go build ./...` and `go
|
||||
// install` produce) must report itself as a development build rather
|
||||
// than as some default release number.
|
||||
// TestDefaultVersionIsDev pins the default: a binary whose linker flags
|
||||
// and build info name no version must report itself as a development
|
||||
// build rather than as some default release number.
|
||||
func TestDefaultVersionIsDev(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -98,3 +106,104 @@ func TestDefaultVersionIsDev(t *testing.T) {
|
||||
globals.DevVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// buildInfo returns build info with the given main module version and,
|
||||
// when revision is not empty, the commit settings Go records for a
|
||||
// build from a git checkout.
|
||||
func buildInfo(version, revision string) *debug.BuildInfo {
|
||||
info := &debug.BuildInfo{
|
||||
Main: debug.Module{Path: "sneak.berlin/go/vaultik", Version: version},
|
||||
}
|
||||
|
||||
if revision != "" {
|
||||
info.Settings = []debug.BuildSetting{
|
||||
{Key: "vcs.revision", Value: revision},
|
||||
{Key: "vcs.time", Value: "2026-11-10T23:12:26Z"},
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// TestFromBuildInfo covers a binary from `go install` of a tag, which
|
||||
// carries no linker flags: it must report the tag. Values the linker
|
||||
// flags set are kept.
|
||||
func TestFromBuildInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
tag = "v1.0.0-rc3"
|
||||
rev = "0123456789abcdef0123456789abcdef01234567"
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
info *debug.BuildInfo
|
||||
version, commit, date string
|
||||
wantVersion, wantCommit string
|
||||
wantDate string
|
||||
}{
|
||||
{
|
||||
name: "go install of a tag",
|
||||
info: buildInfo(tag, ""),
|
||||
version: globals.DevVersion,
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: tag,
|
||||
wantCommit: globals.Unknown,
|
||||
wantDate: globals.Unknown,
|
||||
},
|
||||
{
|
||||
name: "empty version",
|
||||
info: buildInfo(tag, ""),
|
||||
version: "",
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: tag,
|
||||
wantCommit: globals.Unknown,
|
||||
wantDate: globals.Unknown,
|
||||
},
|
||||
{
|
||||
name: "go build in a git checkout",
|
||||
info: buildInfo(tag, rev),
|
||||
version: globals.DevVersion,
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: tag,
|
||||
wantCommit: rev,
|
||||
wantDate: "2026-11-10",
|
||||
},
|
||||
{
|
||||
name: "Go could not tell the version",
|
||||
info: buildInfo("(devel)", ""),
|
||||
version: globals.DevVersion,
|
||||
commit: globals.Unknown,
|
||||
date: globals.Unknown,
|
||||
wantVersion: globals.DevVersion,
|
||||
wantCommit: globals.Unknown,
|
||||
wantDate: globals.Unknown,
|
||||
},
|
||||
{
|
||||
name: "linker flags set",
|
||||
info: buildInfo(tag, rev),
|
||||
version: "v1.0.0-rc2-4-gabcdef1",
|
||||
commit: "abcdef1",
|
||||
date: "2026-11-01",
|
||||
wantVersion: "v1.0.0-rc2-4-gabcdef1",
|
||||
wantCommit: "abcdef1",
|
||||
wantDate: "2026-11-01",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
version, commit, date := globals.FromBuildInfo(
|
||||
tc.info, tc.version, tc.commit, tc.date)
|
||||
|
||||
if version != tc.wantVersion || commit != tc.wantCommit ||
|
||||
date != tc.wantDate {
|
||||
t.Errorf("%s: got %q, %q, %q; want %q, %q, %q", tc.name,
|
||||
version, commit, date,
|
||||
tc.wantVersion, tc.wantCommit, tc.wantDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user