package goversion import ( "fmt" "os/exec" "strconv" "strings" ) // GoVersion represents the Go version of // the Go compiler version used to compile // the target binary. type GoVersion struct { Major int Minor int Rev int // revision number or negative number for beta and rc releases Proposal string Toolchain string } const ( betaStart = -1000 betaEnd = -2000 versionedDevel = -3000 ) func betaRev(beta int) int { return beta + betaEnd } func rcRev(rc int) int { return rc + betaStart } var ( GoVer18Beta = GoVersion{1, 8, betaRev(0), "", ""} ) // Parse parses a go version string func Parse(ver string) (GoVersion, bool) { var r GoVersion var err1, err2, err3 error const devel = "devel" if strings.HasPrefix(ver, devel) { ver = strings.TrimSpace(ver[len(devel):]) if !strings.HasPrefix(ver, "go") { // old development build: devel +COMMIT DATE ARCH return GoVersion{-1, 0, 0, "", ""}, true } // new development build: devel goX.Y-COMMIT DATE ARCH ver = strings.Split(ver[2:], "-")[0] v := strings.SplitN(ver, ".", 2) if len(v) != 2 { return GoVersion{-1, 0, 0, "", ""}, true } major, err1 := strconv.Atoi(v[0]) minor, err2 := strconv.Atoi(v[1]) if err1 != nil || err2 != nil { return GoVersion{-1, 0, 0, "", ""}, true } return GoVersion{major, minor, versionedDevel, "", ""}, true } if strings.HasPrefix(ver, "go") { ver := strings.Split(ver, " ")[0] v := strings.SplitN(ver[2:], ".", 4) switch len(v) { case 2: r.Major, err1 = strconv.Atoi(v[0]) var vr []string if vr = strings.SplitN(v[1], "beta", 2); len(vr) == 2 { // old beta releases goX.YbetaZ var beta int beta, err3 = strconv.Atoi(vr[1]) r.Rev = betaRev(beta) } else if vr = strings.SplitN(v[1], "b", 2); len(vr) == 2 { // old boringcrypto version goX.YbZ if _, err := strconv.Atoi(vr[1]); err != nil { return GoVersion{}, false } } else { vr = strings.SplitN(v[1], "rc", 2) if len(vr) == 2 { // rc release goX.YrcZ var rc int rc, err3 = strconv.Atoi(vr[1]) r.Rev = rcRev(rc) } else { r.Minor, err2 = strconv.Atoi(v[1]) if err2 != nil { return GoVersion{}, false } return r, true } } // old major release (if none of the options above apply) goX.Y r.Minor, err2 = strconv.Atoi(vr[0]) r.Proposal = "" if err1 != nil || err2 != nil || err3 != nil { return GoVersion{}, false } return r, true case 3: r.Major, err1 = strconv.Atoi(v[0]) r.Minor, err2 = strconv.Atoi(v[1]) if vr := strings.SplitN(v[2], "-", 2); len(vr) == 2 { // minor version with toolchain modifier goX.Y.Z-anything r.Rev, err3 = strconv.Atoi(vr[0]) r.Toolchain = vr[1] } else if vr := strings.SplitN(v[2], "b", 2); len(vr) == 2 { // old boringcrypto version goX.Y.ZbW r.Rev, err3 = strconv.Atoi(vr[0]) } else { // minor version goX.Y.Z r.Rev, err3 = strconv.Atoi(v[2]) } r.Proposal = "" if err1 != nil || err2 != nil || err3 != nil { return GoVersion{}, false } return r, true case 4: // old proposal release goX.Y.Z.anything r.Major, err1 = strconv.Atoi(v[0]) r.Minor, err2 = strconv.Atoi(v[1]) r.Rev, err3 = strconv.Atoi(v[2]) r.Proposal = v[3] if err1 != nil || err2 != nil || err3 != nil || r.Proposal == "" { return GoVersion{}, false } return r, true default: return GoVersion{}, false } } return GoVersion{}, false } // AfterOrEqual returns whether one GoVersion is after or // equal to the other. func (v *GoVersion) AfterOrEqual(b GoVersion) bool { if v.Major < b.Major { return false } else if v.Major > b.Major { return true } if v.Minor < b.Minor { return false } else if v.Minor > b.Minor { return true } if v.Rev < b.Rev { return false } else if v.Rev > b.Rev { return true } return true } // IsOldDevel returns whether the GoVersion is an old-style development // build of Go, i.e. without an associated minor and major version. func (v *GoVersion) IsOldDevel() bool { return v.Major < 0 } // IsDevelBuild returns whether the GoVersion is a development build func (v *GoVersion) IsDevelBuild() bool { return v.Major < 0 || v.Rev == versionedDevel } func (v *GoVersion) String() string { switch { case v.Rev < betaStart: // beta version return fmt.Sprintf("go%d.%dbeta%d", v.Major, v.Minor, v.Rev-betaEnd) case v.Rev < 0: // rc version return fmt.Sprintf("go%d.%drc%d", v.Major, v.Minor, v.Rev-betaStart) case v.Proposal != "": // with proposal return fmt.Sprintf("go%d.%d.%d.%s", v.Major, v.Minor, v.Rev, v.Proposal) case v.Rev == 0 && v.Minor < 21: // old major version return fmt.Sprintf("go%d.%d", v.Major, v.Minor) case v.Toolchain != "": return fmt.Sprintf("go%d.%d.%d-%s", v.Major, v.Minor, v.Rev, v.Toolchain) default: // post go1.21 major version or minor version return fmt.Sprintf("go%d.%d.%d", v.Major, v.Minor, v.Rev) } } const goVersionPrefix = "go version " // Installed runs "go version" and parses the output func Installed() (GoVersion, bool) { out, err := exec.Command("go", "version").CombinedOutput() if err != nil { return GoVersion{}, false } s := string(out) if !strings.HasPrefix(s, goVersionPrefix) { return GoVersion{}, false } return Parse(s[len(goVersionPrefix):]) } // VersionAfterOrEqual checks that version (as returned by runtime.Version() // or go version) is major.minor or a later version, or a development // version. func VersionAfterOrEqual(version string, major, minor int) bool { return VersionAfterOrEqualRev(version, major, minor, versionedDevel) } // VersionAfterOrEqualRev checks that version (as returned by runtime.Version() // or go version) is major.minor or a later version, or a development // version. func VersionAfterOrEqualRev(version string, major, minor, rev int) bool { ver, _ := Parse(version) if ver.IsOldDevel() { return true } return ver.AfterOrEqual(GoVersion{major, minor, rev, "", ""}) } const producerVersionPrefix = "Go cmd/compile " // ProducerAfterOrEqual checks that the DW_AT_producer version is // major.minor or a later version, or a development version. func ProducerAfterOrEqual(producer string, major, minor int) bool { ver := ParseProducer(producer) if ver.IsOldDevel() { return true } return ver.AfterOrEqual(GoVersion{major, minor, 0, "", ""}) } func ParseProducer(producer string) GoVersion { producer = strings.TrimPrefix(producer, producerVersionPrefix) ver, _ := Parse(producer) return ver }