package version import ( "fmt" "strconv" "strings" "time" "golang.org/x/mod/semver" ) // Version returns the build information. See build.sh. func Version() (b *BuildInfo, err error) { var n int var s string var sb strings.Builder var ok bool var canonical string var build strings.Builder // Why a map? // I forget but I had a reason for it once upon a time. var raw map[string]string = map[string]string{ "sourceControl": sourceControl, "tag": version, "hash": commitHash, "shortHash": commitShort, "postTagCommits": numCommitsAfterTag, "dirty": isDirty, "time": buildTime, "user": buildUser, "sudoUser": buildSudoUser, "host": buildHost, } var i BuildInfo = BuildInfo{ SourceControl: raw["sourceControl"], TagVersion: raw["tag"], // PostTagCommits: 0, CommitHash: raw["hash"], CommitId: raw["shortHash"], BuildUser: raw["user"], RealBuildUser: raw["sudoUser"], // BuildTime: time.Time{}, BuildHost: raw["host"], Dirty: false, isDefined: false, raw: raw, } if s, ok = raw["postTagCommits"]; ok && strings.TrimSpace(s) != "" { if n, err = strconv.Atoi(s); err == nil { i.PostTagCommits = uint(n) } } if s, ok = raw["time"]; ok && strings.TrimSpace(s) != "" { if n, err = strconv.Atoi(s); err == nil { i.BuildTime = time.Unix(int64(n), 0).UTC() } } switch strings.ToLower(raw["dirty"]) { case "1": i.Dirty = true case "0", "": i.Dirty = false } // Build the short form. We use this for both BuildInfo.short and BuildInfo.verSplit. if i.TagVersion == "" { sb.WriteString(i.SourceControl) } else { sb.WriteString(i.TagVersion) } /* Now the mess. In order to conform to SemVer 2.0 (the spec this code targets): 1.) MAJOR. 2.) MINOR. 3.) PATCH 4.) -PRERELEASE (OPTIONAL) (git commit, if building against a commit made past 1-3. Always included if untagged.) 5.) +BUILDINFO (OPTIONAL) ("+x[.y]", where x is # of commits past 4, or tag commit if 4 is empty. 0 is valid. y is optional, and is the string "dirty" if it is a "dirty" build - that is, uncommitted/unstaged changes. if x and y would be 0 and empty, respectively, then 5 is not included.) 1-3 are already written, or the source control software used if not. Technically 4 and 5 are only included if 3 is present. We force patch to 0 if it's a tagged release and patch isn't present -- so this is not relevant. */ // PRERELEASE if i.TagVersion == "" || i.PostTagCommits > 0 { // We use the full commit hash for git versions, short identifier for tagged releases. if i.TagVersion == "" { i.Pre = i.CommitHash } else { i.Pre = i.CommitId } sb.WriteString(fmt.Sprintf("-%v", i.Pre)) } // BUILD if i.PostTagCommits > 0 || i.Dirty { build.WriteString(strconv.Itoa(int(i.PostTagCommits))) if i.Dirty { build.WriteString(".dirty") } i.Build = build.String() sb.WriteString(fmt.Sprintf("+%v", i.Build)) } i.short = sb.String() if semver.IsValid(i.short) { // DON'T DO THIS. It strips the prerelease and build info. // i.short = semver.Canonical(i.short) // Do this instead. canonical = semver.Canonical(i.short) // Numeric versions... if n, err = strconv.Atoi(strings.TrimPrefix(semver.Major(canonical), "v")); err != nil { err = nil } else { i.Major = uint(n) } if n, err = strconv.Atoi(strings.Split(semver.MajorMinor(canonical), ".")[1]); err != nil { err = nil } else { i.Minor = uint(n) } if n, err = strconv.Atoi(patchReIsolated.FindStringSubmatch(strings.Split(canonical, ".")[2])[1]); err != nil { err = nil } else { i.Patch = uint(n) } // The other tag assignments were performed above. } // The default is 0 for the numerics, so no big deal. i.isDefined = true b = &i return }