Merge pull request #16460 from github/mbg/go/semver-type

Go: Use new type for all semantic versions
This commit is contained in:
Michael B. Gale
2024-06-07 12:19:12 +00:00
committed by GitHub
13 changed files with 386 additions and 180 deletions

View File

@@ -15,7 +15,6 @@ go_library(
"//go/extractor/project",
"//go/extractor/toolchain",
"//go/extractor/util",
"//go/extractor/vendor/golang.org/x/mod/semver",
],
)
@@ -23,4 +22,5 @@ go_test(
name = "autobuilder_test",
srcs = ["build-environment_test.go"],
embed = [":autobuilder"],
deps = ["//go/extractor/util"],
)

View File

@@ -8,53 +8,51 @@ import (
"github.com/github/codeql-go/extractor/diagnostics"
"github.com/github/codeql-go/extractor/project"
"github.com/github/codeql-go/extractor/toolchain"
"golang.org/x/mod/semver"
"github.com/github/codeql-go/extractor/util"
)
const minGoVersion = "1.11"
const maxGoVersion = "1.22"
var minGoVersion = util.NewSemVer("1.11")
var maxGoVersion = util.NewSemVer("1.22")
type versionInfo struct {
goModVersion string // The version of Go found in the go directive in the `go.mod` file.
goModVersionFound bool // Whether a `go` directive was found in the `go.mod` file.
goEnvVersion string // The version of Go found in the environment.
goEnvVersionFound bool // Whether an installation of Go was found in the environment.
goModVersion util.SemVer // The version of Go found in the go directive in the `go.mod` file.
goEnvVersion util.SemVer // The version of Go found in the environment.
}
func (v versionInfo) String() string {
return fmt.Sprintf(
"go.mod version: %s, go.mod directive found: %t, go env version: %s, go installation found: %t",
v.goModVersion, v.goModVersionFound, v.goEnvVersion, v.goEnvVersionFound)
"go.mod version: %s, go env version: %s",
v.goModVersion, v.goEnvVersion)
}
// Check if `version` is lower than `minGoVersion`. Note that for this comparison we ignore the
// patch part of the version, so 1.20.1 and 1.20 are considered equal.
func belowSupportedRange(version string) bool {
return semver.Compare(semver.MajorMinor("v"+version), "v"+minGoVersion) < 0
func belowSupportedRange(version util.SemVer) bool {
return version.MajorMinor().IsOlderThan(minGoVersion.MajorMinor())
}
// Check if `version` is higher than `maxGoVersion`. Note that for this comparison we ignore the
// patch part of the version, so 1.20.1 and 1.20 are considered equal.
func aboveSupportedRange(version string) bool {
return semver.Compare(semver.MajorMinor("v"+version), "v"+maxGoVersion) > 0
func aboveSupportedRange(version util.SemVer) bool {
return version.MajorMinor().IsNewerThan(maxGoVersion.MajorMinor())
}
// Check if `version` is lower than `minGoVersion` or higher than `maxGoVersion`. Note that for
// this comparison we ignore the patch part of the version, so 1.20.1 and 1.20 are considered
// equal.
func outsideSupportedRange(version string) bool {
func outsideSupportedRange(version util.SemVer) bool {
return belowSupportedRange(version) || aboveSupportedRange(version)
}
// Assuming `v.goModVersionFound` is false, emit a diagnostic and return the version to install,
// or the empty string if we should not attempt to install a version of Go.
func getVersionWhenGoModVersionNotFound(v versionInfo) (msg, version string) {
if !v.goEnvVersionFound {
func getVersionWhenGoModVersionNotFound(v versionInfo) (msg string, version util.SemVer) {
if v.goEnvVersion == nil {
// There is no Go version installed in the environment. We have no indication which version
// was intended to be used to build this project. Go versions are generally backwards
// compatible, so we install the maximum supported version.
msg = "No version of Go installed and no `go.mod` file found. Requesting the maximum " +
"supported version of Go (" + maxGoVersion + ")."
"supported version of Go (" + maxGoVersion.String() + ")."
version = maxGoVersion
diagnostics.EmitNoGoModAndNoGoEnv(msg)
} else if outsideSupportedRange(v.goEnvVersion) {
@@ -62,8 +60,8 @@ func getVersionWhenGoModVersionNotFound(v versionInfo) (msg, version string) {
// which version was intended to be used to build this project. Go versions are generally
// backwards compatible, so we install the maximum supported version.
msg = "No `go.mod` file found. The version of Go installed in the environment (" +
v.goEnvVersion + ") is outside of the supported range (" + minGoVersion + "-" +
maxGoVersion + "). Requesting the maximum supported version of Go (" + maxGoVersion +
v.goEnvVersion.String() + ") is outside of the supported range (" + minGoVersion.String() + "-" +
maxGoVersion.String() + "). Requesting the maximum supported version of Go (" + maxGoVersion.String() +
")."
version = maxGoVersion
diagnostics.EmitNoGoModAndGoEnvUnsupported(msg)
@@ -71,9 +69,9 @@ func getVersionWhenGoModVersionNotFound(v versionInfo) (msg, version string) {
// The version of Go that is installed is supported. We have no indication which version
// was intended to be used to build this project. We assume that the installed version is
// suitable and do not install a version of Go.
msg = "No `go.mod` file found. Version " + v.goEnvVersion + " installed in the " +
msg = "No `go.mod` file found. Version " + v.goEnvVersion.String() + " installed in the " +
"environment is supported. Not requesting any version of Go."
version = ""
version = nil
diagnostics.EmitNoGoModAndGoEnvSupported(msg)
}
@@ -82,57 +80,57 @@ func getVersionWhenGoModVersionNotFound(v versionInfo) (msg, version string) {
// Assuming `v.goModVersion` is above the supported range, emit a diagnostic and return the
// version to install, or the empty string if we should not attempt to install a version of Go.
func getVersionWhenGoModVersionTooHigh(v versionInfo) (msg, version string) {
if !v.goEnvVersionFound {
func getVersionWhenGoModVersionTooHigh(v versionInfo) (msg string, version util.SemVer) {
if v.goEnvVersion == nil {
// The version in the `go.mod` file is above the supported range. There is no Go version
// installed. We install the maximum supported version as a best effort.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is above the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). No version of Go installed. Requesting the maximum supported version of Go (" +
maxGoVersion + ")."
maxGoVersion.String() + ")."
version = maxGoVersion
diagnostics.EmitGoModVersionTooHighAndNoGoEnv(msg)
} else if aboveSupportedRange(v.goEnvVersion) {
// The version in the `go.mod` file is above the supported range. The version of Go that
// is installed is above the supported range. We do not install a version of Go.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
"). The version of Go installed in the environment (" + v.goEnvVersion +
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is above the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is above the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). Not requesting any version of Go."
version = ""
version = nil
diagnostics.EmitGoModVersionTooHighAndEnvVersionTooHigh(msg)
} else if belowSupportedRange(v.goEnvVersion) {
// The version in the `go.mod` file is above the supported range. The version of Go that
// is installed is below the supported range. We install the maximum supported version as
// a best effort.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
"). The version of Go installed in the environment (" + v.goEnvVersion +
") is below the supported range (" + minGoVersion + "-" + maxGoVersion +
"). Requesting the maximum supported version of Go (" + maxGoVersion + ")."
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is above the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is below the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). Requesting the maximum supported version of Go (" + maxGoVersion.String() + ")."
version = maxGoVersion
diagnostics.EmitGoModVersionTooHighAndEnvVersionTooLow(msg)
} else if semver.Compare("v"+maxGoVersion, "v"+v.goEnvVersion) > 0 {
} else if maxGoVersion.IsNewerThan(v.goEnvVersion) {
// The version in the `go.mod` file is above the supported range. The version of Go that
// is installed is supported and below the maximum supported version. We install the
// maximum supported version as a best effort.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
"). The version of Go installed in the environment (" + v.goEnvVersion +
") is below the maximum supported version (" + maxGoVersion +
"). Requesting the maximum supported version of Go (" + maxGoVersion + ")."
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is above the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is below the maximum supported version (" + maxGoVersion.String() +
"). Requesting the maximum supported version of Go (" + maxGoVersion.String() + ")."
version = maxGoVersion
diagnostics.EmitGoModVersionTooHighAndEnvVersionBelowMax(msg)
} else {
// The version in the `go.mod` file is above the supported range. The version of Go that
// is installed is the maximum supported version. We do not install a version of Go.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
"). The version of Go installed in the environment (" + v.goEnvVersion +
") is the maximum supported version (" + maxGoVersion +
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is above the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is the maximum supported version (" + maxGoVersion.String() +
"). Not requesting any version of Go."
version = ""
version = nil
diagnostics.EmitGoModVersionTooHighAndEnvVersionMax(msg)
}
@@ -141,35 +139,35 @@ func getVersionWhenGoModVersionTooHigh(v versionInfo) (msg, version string) {
// Assuming `v.goModVersion` is below the supported range, emit a diagnostic and return the
// version to install, or the empty string if we should not attempt to install a version of Go.
func getVersionWhenGoModVersionTooLow(v versionInfo) (msg, version string) {
if !v.goEnvVersionFound {
func getVersionWhenGoModVersionTooLow(v versionInfo) (msg string, version util.SemVer) {
if v.goEnvVersion == nil {
// There is no Go version installed. The version in the `go.mod` file is below the
// supported range. Go versions are generally backwards compatible, so we install the
// minimum supported version.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is below the supported range (" + minGoVersion + "-" + maxGoVersion +
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is below the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). No version of Go installed. Requesting the minimum supported version of Go (" +
minGoVersion + ")."
minGoVersion.String() + ")."
version = minGoVersion
diagnostics.EmitGoModVersionTooLowAndNoGoEnv(msg)
} else if outsideSupportedRange(v.goEnvVersion) {
// The version of Go that is installed is outside of the supported range. The version
// in the `go.mod` file is below the supported range. Go versions are generally
// backwards compatible, so we install the minimum supported version.
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
") is below the supported range (" + minGoVersion + "-" + maxGoVersion +
"). The version of Go installed in the environment (" + v.goEnvVersion +
") is outside of the supported range (" + minGoVersion + "-" + maxGoVersion + "). " +
"Requesting the minimum supported version of Go (" + minGoVersion + ")."
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion.String() +
") is below the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() +
"). The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is outside of the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() + "). " +
"Requesting the minimum supported version of Go (" + minGoVersion.String() + ")."
version = minGoVersion
diagnostics.EmitGoModVersionTooLowAndEnvVersionUnsupported(msg)
} else {
// The version of Go that is installed is supported. The version in the `go.mod` file is
// below the supported range. We do not install a version of Go.
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
msg = "The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is supported and is high enough for the version found in the `go.mod` file (" +
v.goModVersion + "). Not requesting any version of Go."
version = ""
v.goModVersion.String() + "). Not requesting any version of Go."
version = nil
diagnostics.EmitGoModVersionTooLowAndEnvVersionSupported(msg)
}
@@ -178,40 +176,40 @@ func getVersionWhenGoModVersionTooLow(v versionInfo) (msg, version string) {
// Assuming `v.goModVersion` is in the supported range, emit a diagnostic and return the version
// to install, or the empty string if we should not attempt to install a version of Go.
func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
if !v.goEnvVersionFound {
func getVersionWhenGoModVersionSupported(v versionInfo) (msg string, version util.SemVer) {
if v.goEnvVersion == nil {
// There is no Go version installed. The version in the `go.mod` file is supported.
// We install the version from the `go.mod` file.
msg = "No version of Go installed. Requesting the version of Go found in the `go.mod` " +
"file (" + v.goModVersion + ")."
"file (" + v.goModVersion.String() + ")."
version = v.goModVersion
diagnostics.EmitGoModVersionSupportedAndNoGoEnv(msg)
} else if outsideSupportedRange(v.goEnvVersion) {
// The version of Go that is installed is outside of the supported range. The version in
// the `go.mod` file is supported. We install the version from the `go.mod` file.
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
") is outside of the supported range (" + minGoVersion + "-" + maxGoVersion + "). " +
msg = "The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is outside of the supported range (" + minGoVersion.String() + "-" + maxGoVersion.String() + "). " +
"Requesting the version of Go from the `go.mod` file (" +
v.goModVersion + ")."
v.goModVersion.String() + ")."
version = v.goModVersion
diagnostics.EmitGoModVersionSupportedAndGoEnvUnsupported(msg)
} else if semver.Compare("v"+v.goModVersion, "v"+v.goEnvVersion) > 0 {
} else if v.goModVersion.IsNewerThan(v.goEnvVersion) {
// The version of Go that is installed is supported. The version in the `go.mod` file is
// supported and is higher than the version that is installed. We install the version from
// the `go.mod` file.
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
") is lower than the version found in the `go.mod` file (" + v.goModVersion +
"). Requesting the version of Go from the `go.mod` file (" + v.goModVersion + ")."
msg = "The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is lower than the version found in the `go.mod` file (" + v.goModVersion.String() +
"). Requesting the version of Go from the `go.mod` file (" + v.goModVersion.String() + ")."
version = v.goModVersion
diagnostics.EmitGoModVersionSupportedHigherGoEnv(msg)
} else {
// The version of Go that is installed is supported. The version in the `go.mod` file is
// supported and is lower than or equal to the version that is installed. We do not install
// a version of Go.
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
msg = "The version of Go installed in the environment (" + v.goEnvVersion.String() +
") is supported and is high enough for the version found in the `go.mod` file (" +
v.goModVersion + "). Not requesting any version of Go."
version = ""
v.goModVersion.String() + "). Not requesting any version of Go."
version = nil
diagnostics.EmitGoModVersionSupportedLowerEqualGoEnv(msg)
}
@@ -231,8 +229,8 @@ func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
// | *In supported range* | No action | No action | Install version from go.mod if newer than installed | Install max supported if newer than installed |
// | *Above max supported* | Install max supported | Install min supported | Install version from go.mod | No action |
// +-----------------------+-----------------------+-----------------------+-----------------------------------------------------+------------------------------------------------+
func getVersionToInstall(v versionInfo) (msg, version string) {
if !v.goModVersionFound {
func getVersionToInstall(v versionInfo) (msg string, version util.SemVer) {
if v.goModVersion == nil {
return getVersionWhenGoModVersionNotFound(v)
}
@@ -249,12 +247,12 @@ func getVersionToInstall(v versionInfo) (msg, version string) {
// Output some JSON to stdout specifying the version of Go to install, unless `version` is the
// empty string.
func outputEnvironmentJson(version string) {
func outputEnvironmentJson(version util.SemVer) {
var content string
if version == "" {
if version == nil {
content = `{ "go": {} }`
} else {
content = `{ "go": { "version": "` + version + `" } }`
content = `{ "go": { "version": "` + version.StandardSemVer() + `" } }`
}
_, err := fmt.Fprint(os.Stdout, content)
@@ -273,13 +271,11 @@ func IdentifyEnvironment() {
defer project.RemoveTemporaryExtractorFiles()
// Find the greatest Go version required by any of the workspaces.
greatestGoVersion := project.RequiredGoVersion(&workspaces)
v.goModVersion, v.goModVersionFound = greatestGoVersion.Version, greatestGoVersion.Found
v.goModVersion = project.RequiredGoVersion(&workspaces)
// Find which, if any, version of Go is installed on the system already.
v.goEnvVersionFound = toolchain.IsInstalled()
if v.goEnvVersionFound {
v.goEnvVersion = toolchain.GetEnvGoVersion()[2:]
if toolchain.IsInstalled() {
v.goEnvVersion = toolchain.GetEnvGoSemVer()
}
// Determine which version of Go we should recommend to install.

View File

@@ -1,47 +1,55 @@
package autobuilder
import "testing"
import (
"testing"
"github.com/github/codeql-go/extractor/util"
)
func TestGetVersionToInstall(t *testing.T) {
tests := map[versionInfo]string{
type inputVersions struct {
modVersion string
envVersion string
}
tests := map[inputVersions]string{
// getVersionWhenGoModVersionNotFound()
{"", false, "", false}: maxGoVersion,
{"", false, "1.2.2", true}: maxGoVersion,
{"", false, "9999.0.1", true}: maxGoVersion,
{"", false, "1.11.13", true}: "",
{"", false, "1.20.3", true}: "",
{"", ""}: maxGoVersion.String(),
{"", "1.2.2"}: maxGoVersion.String(),
{"", "9999.0.1"}: maxGoVersion.String(),
{"", "1.11.13"}: "",
{"", "1.20.3"}: "",
// getVersionWhenGoModVersionTooHigh()
{"9999.0", true, "", false}: maxGoVersion,
{"9999.0", true, "9999.0.1", true}: "",
{"9999.0", true, "1.1", true}: maxGoVersion,
{"9999.0", true, minGoVersion, false}: maxGoVersion,
{"9999.0", true, maxGoVersion, true}: "",
{"9999.0", ""}: maxGoVersion.String(),
{"9999.0", "9999.0.1"}: "",
{"9999.0", "1.1"}: maxGoVersion.String(),
{"9999.0", minGoVersion.String()}: maxGoVersion.String(),
{"9999.0", maxGoVersion.String()}: "",
// getVersionWhenGoModVersionTooLow()
{"0.0", true, "", false}: minGoVersion,
{"0.0", true, "9999.0", true}: minGoVersion,
{"0.0", true, "1.2.2", true}: minGoVersion,
{"0.0", true, "1.20.3", true}: "",
{"0.0", ""}: minGoVersion.String(),
{"0.0", "9999.0"}: minGoVersion.String(),
{"0.0", "1.2.2"}: minGoVersion.String(),
{"0.0", "1.20.3"}: "",
// getVersionWhenGoModVersionSupported()
{"1.20", true, "", false}: "1.20",
{"1.11", true, "", false}: "1.11",
{"1.20", true, "1.2.2", true}: "1.20",
{"1.11", true, "1.2.2", true}: "1.11",
{"1.20", true, "9999.0.1", true}: "1.20",
{"1.11", true, "9999.0.1", true}: "1.11",
{"1.20", ""}: "1.20",
{"1.11", ""}: "1.11",
{"1.20", "1.2.2"}: "1.20",
{"1.11", "1.2.2"}: "1.11",
{"1.20", "9999.0.1"}: "1.20",
{"1.11", "9999.0.1"}: "1.11",
// go.mod version > go installation version
{"1.20", true, "1.11.13", true}: "1.20",
{"1.20", true, "1.12", true}: "1.20",
{"1.20", "1.11.13"}: "1.20",
{"1.20", "1.12"}: "1.20",
// go.mod version <= go installation version (Note comparisons ignore the patch version)
{"1.11", true, "1.20", true}: "",
{"1.11", true, "1.20.3", true}: "",
{"1.20", true, "1.20.3", true}: "",
{"1.11", "1.20"}: "",
{"1.11", "1.20.3"}: "",
{"1.20", "1.20.3"}: "",
}
for input, expected := range tests {
_, actual := getVersionToInstall(input)
if actual != expected {
_, actual := getVersionToInstall(versionInfo{util.NewSemVer(input.modVersion), util.NewSemVer(input.envVersion)})
if actual != util.NewSemVer(expected) {
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
}
}

View File

@@ -14,7 +14,6 @@ go_library(
"//go/extractor/project",
"//go/extractor/toolchain",
"//go/extractor/util",
"//go/extractor/vendor/golang.org/x/mod/semver",
],
)

View File

@@ -10,8 +10,6 @@ import (
"runtime"
"strings"
"golang.org/x/mod/semver"
"github.com/github/codeql-go/extractor/autobuilder"
"github.com/github/codeql-go/extractor/diagnostics"
"github.com/github/codeql-go/extractor/project"
@@ -156,7 +154,7 @@ func getNeedGopath(workspace project.GoWorkspace, importpath string) bool {
// Try to update `go.mod` and `go.sum` if the go version is >= 1.16.
func tryUpdateGoModAndGoSum(workspace project.GoWorkspace) {
// Go 1.16 and later won't automatically attempt to update go.mod / go.sum during package loading, so try to update them here:
if workspace.ModMode != project.ModVendor && workspace.DepMode == project.GoGetWithModules && semver.Compare(toolchain.GetEnvGoSemVer(), "v1.16") >= 0 {
if workspace.ModMode != project.ModVendor && workspace.DepMode == project.GoGetWithModules && toolchain.GetEnvGoSemVer().IsAtLeast(toolchain.V1_16) {
for _, goMod := range workspace.Modules {
// stat go.mod and go.sum
goModPath := goMod.Path
@@ -542,12 +540,12 @@ func installDependenciesAndBuild() {
// This diagnostic is not required if the system Go version is 1.21 or greater, since the
// Go tooling should install required Go versions as needed.
if semver.Compare(toolchain.GetEnvGoSemVer(), "v1.21.0") < 0 && greatestGoVersion.Found && semver.Compare("v"+greatestGoVersion.Version, toolchain.GetEnvGoSemVer()) > 0 {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer(), "v"+greatestGoVersion.Version)
if toolchain.GetEnvGoSemVer().IsOlderThan(toolchain.V1_21) && greatestGoVersion != nil && greatestGoVersion.IsNewerThan(toolchain.GetEnvGoSemVer()) {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer().String(), greatestGoVersion.String())
if val, _ := os.LookupEnv("GITHUB_ACTIONS"); val == "true" {
log.Printf(
"A go.mod file requires version %s of Go, but version %s is installed. Consider adding an actions/setup-go step to your workflow.\n",
"v"+greatestGoVersion.Version,
greatestGoVersion,
toolchain.GetEnvGoSemVer())
}
}
@@ -559,7 +557,7 @@ func installDependenciesAndBuild() {
for i, workspace := range workspaces {
goVersionInfo := workspace.RequiredGoVersion()
fixGoVendorIssues(&workspace, goVersionInfo.Found)
fixGoVendorIssues(&workspace, goVersionInfo != nil)
tryUpdateGoModAndGoSum(workspace)

View File

@@ -12,7 +12,6 @@ go_library(
"//go/extractor/toolchain",
"//go/extractor/util",
"//go/extractor/vendor/golang.org/x/mod/modfile",
"//go/extractor/vendor/golang.org/x/mod/semver",
],
)

View File

@@ -13,7 +13,6 @@ import (
"github.com/github/codeql-go/extractor/toolchain"
"github.com/github/codeql-go/extractor/util"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
)
// DependencyInstallerMode is an enum describing how dependencies should be installed
@@ -49,53 +48,47 @@ type GoWorkspace struct {
}
// Represents a nullable version string.
type GoVersionInfo struct {
// The version string, if any
Version string
// A value indicating whether a version string was found
Found bool
}
type GoVersionInfo = util.SemVer
// Determines the version of Go that is required by this workspace. This is, in order of preference:
// 1. The Go version specified in the `go.work` file, if any.
// 2. The greatest Go version specified in any `go.mod` file, if any.
func (workspace *GoWorkspace) RequiredGoVersion() GoVersionInfo {
func (workspace *GoWorkspace) RequiredGoVersion() util.SemVer {
if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Go != nil {
// If we have parsed a `go.work` file, return the version number from it.
return GoVersionInfo{Version: workspace.WorkspaceFile.Go.Version, Found: true}
return util.NewSemVer(workspace.WorkspaceFile.Go.Version)
} else if workspace.Modules != nil && len(workspace.Modules) > 0 {
// Otherwise, if we have `go.work` files, find the greatest Go version in those.
var greatestVersion string = ""
var greatestVersion util.SemVer = nil
for _, module := range workspace.Modules {
if module.Module != nil && module.Module.Go != nil {
// If we have parsed the file, retrieve the version number we have already obtained.
if greatestVersion == "" || semver.Compare("v"+module.Module.Go.Version, "v"+greatestVersion) > 0 {
greatestVersion = module.Module.Go.Version
modVersion := util.NewSemVer(module.Module.Go.Version)
if greatestVersion == nil || modVersion.IsNewerThan(greatestVersion) {
greatestVersion = modVersion
}
} else {
modVersion := tryReadGoDirective(module.Path)
if modVersion.Found && (greatestVersion == "" || semver.Compare("v"+modVersion.Version, "v"+greatestVersion) > 0) {
greatestVersion = modVersion.Version
if modVersion != nil && (greatestVersion == nil || modVersion.IsNewerThan(greatestVersion)) {
greatestVersion = modVersion
}
}
}
// If we have found some version, return it.
if greatestVersion != "" {
return GoVersionInfo{Version: greatestVersion, Found: true}
}
return greatestVersion
}
return GoVersionInfo{Version: "", Found: false}
return nil
}
// Finds the greatest Go version required by any of the given `workspaces`.
// Returns a `GoVersionInfo` value with `Found: false` if no version information is available.
func RequiredGoVersion(workspaces *[]GoWorkspace) GoVersionInfo {
greatestGoVersion := GoVersionInfo{Version: "", Found: false}
func RequiredGoVersion(workspaces *[]GoWorkspace) util.SemVer {
var greatestGoVersion util.SemVer = nil
for _, workspace := range *workspaces {
goVersionInfo := workspace.RequiredGoVersion()
if goVersionInfo.Found && (!greatestGoVersion.Found || semver.Compare("v"+goVersionInfo.Version, "v"+greatestGoVersion.Version) > 0) {
if goVersionInfo != nil && (greatestGoVersion == nil || goVersionInfo.IsNewerThan(greatestGoVersion)) {
greatestGoVersion = goVersionInfo
}
}
@@ -183,7 +176,7 @@ var toolchainVersionRe *regexp.Regexp = regexp.MustCompile(`(?m)^([0-9]+\.[0-9]+
// there is no `toolchain` directive, and the Go language version is not a valid toolchain version.
func hasInvalidToolchainVersion(modFile *modfile.File) bool {
return modFile.Toolchain == nil && modFile.Go != nil &&
!toolchainVersionRe.Match([]byte(modFile.Go.Version)) && semver.Compare("v"+modFile.Go.Version, "v1.21.0") >= 0
!toolchainVersionRe.Match([]byte(modFile.Go.Version)) && util.NewSemVer(modFile.Go.Version).IsAtLeast(toolchain.V1_21)
}
// Given a list of `go.mod` file paths, try to parse them all. The resulting array of `GoModule` objects
@@ -537,17 +530,14 @@ const (
// argsForGoVersion returns the arguments to pass to the Go compiler for the given `ModMode` and
// Go version
func (m ModMode) ArgsForGoVersion(version string) []string {
func (m ModMode) ArgsForGoVersion(version util.SemVer) []string {
switch m {
case ModUnset:
return []string{}
case ModReadonly:
return []string{"-mod=readonly"}
case ModMod:
if !semver.IsValid(version) {
log.Fatalf("Invalid Go semver: '%s'", version)
}
if semver.Compare(version, "v1.14") < 0 {
if version.IsOlderThan(toolchain.V1_14) {
return []string{} // -mod=mod is the default behaviour for go <= 1.13, and is not accepted as an argument
} else {
return []string{"-mod=mod"}
@@ -574,7 +564,7 @@ func getModMode(depMode DependencyInstallerMode, baseDir string) ModMode {
// Tries to open `go.mod` and read a go directive, returning the version and whether it was found.
// The version string is returned in the "1.2.3" format.
func tryReadGoDirective(path string) GoVersionInfo {
func tryReadGoDirective(path string) util.SemVer {
versionRe := regexp.MustCompile(`(?m)^go[ \t\r]+([0-9]+\.[0-9]+(\.[0-9]+)?)`)
goMod, err := os.ReadFile(path)
if err != nil {
@@ -583,9 +573,9 @@ func tryReadGoDirective(path string) GoVersionInfo {
matches := versionRe.FindSubmatch(goMod)
if matches != nil {
if len(matches) > 1 {
return GoVersionInfo{string(matches[1]), true}
return util.NewSemVer(string(matches[1]))
}
}
}
return GoVersionInfo{"", false}
return nil
}

View File

@@ -7,14 +7,12 @@ go_library(
srcs = ["toolchain.go"],
importpath = "github.com/github/codeql-go/extractor/toolchain",
visibility = ["//visibility:public"],
deps = [
"//go/extractor/util",
"//go/extractor/vendor/golang.org/x/mod/semver",
],
deps = ["//go/extractor/util"],
)
go_test(
name = "toolchain_test",
srcs = ["toolchain_test.go"],
embed = [":toolchain"],
deps = ["//go/extractor/util"],
)

View File

@@ -11,9 +11,13 @@ import (
"strings"
"github.com/github/codeql-go/extractor/util"
"golang.org/x/mod/semver"
)
var V1_14 = util.NewSemVer("v1.14.0")
var V1_16 = util.NewSemVer("v1.16.0")
var V1_18 = util.NewSemVer("v1.18.0")
var V1_21 = util.NewSemVer("v1.21.0")
// Check if Go is installed in the environment.
func IsInstalled() bool {
_, err := exec.LookPath("go")
@@ -23,11 +27,11 @@ func IsInstalled() bool {
// The default Go version that is available on a system and a set of all versions
// that we know are installed on the system.
var goVersion = ""
var goVersions = map[string]struct{}{}
var goVersions = map[util.SemVer]struct{}{}
// Adds an entry to the set of installed Go versions for the normalised `version` number.
func addGoVersion(version string) {
goVersions[semver.Canonical("v"+version)] = struct{}{}
func addGoVersion(version util.SemVer) {
goVersions[version] = struct{}{}
}
// Returns the current Go version as returned by 'go version', e.g. go1.14.4
@@ -53,19 +57,19 @@ func GetEnvGoVersion() string {
}
goVersion = parseGoVersion(string(out))
addGoVersion(goVersion[2:])
addGoVersion(util.NewSemVer(goVersion))
}
return goVersion
}
// Determines whether, to our knowledge, `version` is available on the current system.
func HasGoVersion(version string) bool {
_, found := goVersions[semver.Canonical("v"+version)]
func HasGoVersion(version util.SemVer) bool {
_, found := goVersions[version]
return found
}
// Attempts to install the Go toolchain `version`.
func InstallVersion(workingDir string, version string) bool {
func InstallVersion(workingDir string, version util.SemVer) bool {
// No need to install it if we know that it is already installed.
if HasGoVersion(version) {
return true
@@ -74,7 +78,7 @@ func InstallVersion(workingDir string, version string) bool {
// Construct a command to invoke `go version` with `GOTOOLCHAIN=go1.N.0` to give
// Go a valid toolchain version to download the toolchain we need; subsequent commands
// should then work even with an invalid version that's still in `go.mod`
toolchainArg := "GOTOOLCHAIN=go" + semver.Canonical("v" + version)[1:]
toolchainArg := "GOTOOLCHAIN=go" + version.String()[1:]
versionCmd := Version()
versionCmd.Dir = workingDir
versionCmd.Env = append(os.Environ(), toolchainArg)
@@ -107,20 +111,12 @@ func InstallVersion(workingDir string, version string) bool {
}
// Returns the current Go version in semver format, e.g. v1.14.4
func GetEnvGoSemVer() string {
func GetEnvGoSemVer() util.SemVer {
goVersion := GetEnvGoVersion()
if !strings.HasPrefix(goVersion, "go") {
log.Fatalf("Expected 'go version' output of the form 'go1.2.3'; got '%s'", goVersion)
}
// Go versions don't follow the SemVer format, but the only exception we normally care about
// is release candidates; so this is a horrible hack to convert e.g. `go1.22rc1` into `go1.22-rc1`
// which is compatible with the SemVer specification
rcIndex := strings.Index(goVersion, "rc")
if rcIndex != -1 {
return semver.Canonical("v"+goVersion[2:rcIndex]) + "-" + goVersion[rcIndex:]
} else {
return semver.Canonical("v" + goVersion[2:])
}
return util.NewSemVer(goVersion)
}
// The 'go version' command may output warnings on separate lines before
@@ -137,7 +133,7 @@ func parseGoVersion(data string) string {
// Returns a value indicating whether the system Go toolchain supports workspaces.
func SupportsWorkspaces() bool {
return semver.Compare(GetEnvGoSemVer(), "v1.18.0") >= 0
return GetEnvGoSemVer().IsAtLeast(V1_18)
}
// Run `go mod tidy -e` in the directory given by `path`.

View File

@@ -1,6 +1,10 @@
package toolchain
import "testing"
import (
"testing"
"github.com/github/codeql-go/extractor/util"
)
func TestParseGoVersion(t *testing.T) {
tests := map[string]string{
@@ -16,7 +20,7 @@ func TestParseGoVersion(t *testing.T) {
}
func TestHasGoVersion(t *testing.T) {
if HasGoVersion("1.21") {
if HasGoVersion(util.NewSemVer("1.21")) {
t.Error("Expected HasGoVersion(\"1.21\") to be false, but got true")
}
}

View File

@@ -4,13 +4,21 @@ load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "util",
srcs = ["util.go"],
srcs = [
"semver.go",
"util.go",
],
importpath = "github.com/github/codeql-go/extractor/util",
visibility = ["//visibility:public"],
deps = ["//go/extractor/vendor/golang.org/x/mod/semver"],
)
go_test(
name = "util_test",
srcs = ["util_test.go"],
srcs = [
"semver_test.go",
"util_test.go",
],
embed = [":util"],
deps = ["//go/extractor/vendor/golang.org/x/mod/semver"],
)

141
go/extractor/util/semver.go Normal file
View File

@@ -0,0 +1,141 @@
package util
import (
"log"
"strings"
"golang.org/x/mod/semver"
)
// A type used to represent values known to be valid semantic versions.
type SemVer interface {
String() string
// Compares this semantic version against the `other`. Returns the following values:
//
// 0 if both versions are equal.
//
// -1 if this version is older than the `other`.
//
// 1 if this version is newer than the `other`.
Compare(other SemVer) int
// Returns true if this version is newer than the `other`, or false otherwise.
IsNewerThan(other SemVer) bool
// Returns true if this version is equal to `other` or newer, or false otherwise.
IsAtLeast(other SemVer) bool
// Returns true if this version is older than the `other`, or false otherwise.
IsOlderThan(other SemVer) bool
// Returns true if this version is equal to `other` or older, or false otherwise.
IsAtMost(other SemVer) bool
// Returns the `major.minor` version prefix of the semantic version. For example, "v1.2.3" becomes "v1.2".
MajorMinor() SemVer
// Renders the semantic version as a standard version string, i.e. without a leading "v".
StandardSemVer() string
}
// The internal representation used for values known to be valid semantic versions.
//
// NOTE: Not exported to prevent invalid values from being constructed.
type semVer string
// Converts the semantic version to a string representation.
func (ver semVer) String() string {
return string(ver)
}
// Represents `v0.0.0`.
func Zero() SemVer {
return semVer("v0.0.0")
}
// Constructs a [SemVer] from the given `version` string. The input can be any valid version string
// that we commonly deal with. This includes ordinary version strings such as "1.2.3", ones with
// the "go" prefix, and ones with the "v" prefix. Go's non-semver-compliant release candidate
// versions are also automatically corrected from e.g. "go1.20rc1" to "v1.20-rc1". If given
// the empty string, this function return `nil`. Otherwise, for invalid version strings, the function
// prints a message to the log and exits the process.
//
// Note that we deliberately do not format the resulting [SemVer] to be in a `Canonical` representation.
// This is because we want to maintain the input version specificity for as long as possible. This is useful
// for e.g. `IdentifyEnvironment` where we want to output "1.22" if the project specifies "1.22" as the
// required Go version, rather than outputting "1.22.0", which implies a specific patch-level version
// when the intention is that any patch-level version of "1.22" is acceptable.
func NewSemVer(version string) SemVer {
// If the input is the empty string, return `nil` since we use `nil` to represent "no version".
if version == "" {
return nil
}
// Drop a "go" prefix, if there is one.
version = strings.TrimPrefix(version, "go")
// Go versions don't follow the SemVer format, but the only exception we normally care about
// is release candidates; so this is a horrible hack to convert e.g. `1.22rc1` into `1.22-rc1`
// which is compatible with the SemVer specification.
rcIndex := strings.Index(version, "rc")
if rcIndex != -1 {
var numeric string
prerelease := version[rcIndex:]
// the version string may already contain a "-";
// if it does, drop the "-" since we add it back later
if version[rcIndex-1] != '-' {
numeric = version[:rcIndex]
} else {
numeric = version[:rcIndex-1]
}
// add a "v" to the numeric part of the version, if it's not already there
if !strings.HasPrefix(numeric, "v") {
numeric = "v" + numeric
}
// for the semver library to accept a version containing a prerelease,
// the numeric part must be canonical; e.g.. "v0-rc1" is not valid and
// must be "v0.0.0-rc1" instead.
version = semver.Canonical(numeric) + "-" + prerelease
} else if !strings.HasPrefix(version, "v") {
// Add the "v" prefix that is required by the `semver` package, if
// it's not already there.
version = "v" + version
}
// Check that the remaining version string is valid.
if !semver.IsValid(version) {
log.Fatalf("%s is not a valid version string\n", version)
}
return semVer(version)
}
func (ver semVer) Compare(other SemVer) int {
return semver.Compare(string(ver), string(other.String()))
}
func (ver semVer) IsNewerThan(other SemVer) bool {
return ver.Compare(other) > 0
}
func (ver semVer) IsAtLeast(other SemVer) bool {
return ver.Compare(other) >= 0
}
func (ver semVer) IsOlderThan(other SemVer) bool {
return ver.Compare(other) < 0
}
func (ver semVer) IsAtMost(other SemVer) bool {
return ver.Compare(other) <= 0
}
func (ver semVer) MajorMinor() SemVer {
return semVer(semver.MajorMinor(string(ver)))
}
func (ver semVer) StandardSemVer() string {
// Drop the 'v' prefix from the version string.
result := string(ver)[1:]
// Correct the pre-release identifier for use with `setup-go`, if one is present.
// This still remains a standard semantic version.
return strings.Replace(result, "-rc", "-rc.", 1)
}

View File

@@ -0,0 +1,69 @@
package util
import (
"strings"
"testing"
"golang.org/x/mod/semver"
)
func TestNewSemVer(t *testing.T) {
type TestPair struct {
Input string
Expected string
}
// Check the special case for the empty string.
result := NewSemVer("")
if result != nil {
t.Errorf("Expected NewSemVer(\"\") to return nil, but got \"%s\".", result)
}
testData := []TestPair{
{"0", "v0"},
{"1.0", "v1.0"},
{"1.0.2", "v1.0.2"},
{"1.20", "v1.20"},
{"1.22.3", "v1.22.3"},
}
// prefixes should not affect the result
prefixes := []string{"", "go", "v"}
// suffixes
suffixes := []string{"", "rc1", "-rc1"}
// Check that we get what we expect for each of the test cases.
for _, pair := range testData {
for _, prefix := range prefixes {
for _, suffix := range suffixes {
// combine the input string with the current prefix and suffix
input := prefix + pair.Input + suffix
result := NewSemVer(input)
expected := pair.Expected
if suffix != "" {
expected = semver.Canonical(pair.Expected) + "-rc1"
}
if result.String() != expected {
t.Errorf(
"Expected NewSemVer(\"%s\") to return \"%s\", but got \"%s\".",
input,
expected,
result,
)
}
expected = strings.Replace(expected, "-rc1", "-rc.1", 1)
if result.StandardSemVer() != expected[1:] {
t.Errorf(
"Expected NewSemVer(\"%s\").StandardSemVer() to return \"%s\", but got \"%s\".",
input,
expected[1:],
result.StandardSemVer(),
)
}
}
}
}
}