Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go: Use toolchain directives for version selection if available, and add tests #16453

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions go/extractor/autobuilder/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 15 additions & 14 deletions go/extractor/autobuilder/build-environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/github/codeql-go/extractor/diagnostics"
"github.com/github/codeql-go/extractor/project"
"github.com/github/codeql-go/extractor/toolchain"
"github.com/github/codeql-go/extractor/util"
"golang.org/x/mod/semver"
)

Expand All @@ -30,13 +31,13 @@ func (v versionInfo) String() string {
// 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
return semver.Compare(semver.MajorMinor(util.FormatSemVer(version)), util.FormatSemVer(minGoVersion)) < 0
}

// 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
return semver.Compare(semver.MajorMinor(util.FormatSemVer(version)), util.FormatSemVer(maxGoVersion)) > 0
}

// Check if `version` is lower than `minGoVersion` or higher than `maxGoVersion`. Note that for
Expand Down Expand Up @@ -113,7 +114,7 @@ func getVersionWhenGoModVersionTooHigh(v versionInfo) (msg, version string) {
"). Requesting the maximum supported version of Go (" + maxGoVersion + ")."
version = maxGoVersion
diagnostics.EmitGoModVersionTooHighAndEnvVersionTooLow(msg)
} else if semver.Compare("v"+maxGoVersion, "v"+v.goEnvVersion) > 0 {
} else if semver.Compare(util.FormatSemVer(maxGoVersion), util.FormatSemVer(v.goEnvVersion)) > 0 {
// 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.
Expand Down Expand Up @@ -195,7 +196,7 @@ func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
v.goModVersion + ")."
version = v.goModVersion
diagnostics.EmitGoModVersionSupportedAndGoEnvUnsupported(msg)
} else if semver.Compare("v"+v.goModVersion, "v"+v.goEnvVersion) > 0 {
} else if semver.Compare(util.FormatSemVer(v.goModVersion), util.FormatSemVer(v.goEnvVersion)) > 0 {
// 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.
Expand Down Expand Up @@ -233,18 +234,18 @@ func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
// +-----------------------+-----------------------+-----------------------+-----------------------------------------------------+------------------------------------------------+
func getVersionToInstall(v versionInfo) (msg, version string) {
if !v.goModVersionFound {
return getVersionWhenGoModVersionNotFound(v)
}

if aboveSupportedRange(v.goModVersion) {
return getVersionWhenGoModVersionTooHigh(v)
}

if belowSupportedRange(v.goModVersion) {
return getVersionWhenGoModVersionTooLow(v)
msg, version = getVersionWhenGoModVersionNotFound(v)
} else if aboveSupportedRange(v.goModVersion) {
msg, version = getVersionWhenGoModVersionTooHigh(v)
} else if belowSupportedRange(v.goModVersion) {
msg, version = getVersionWhenGoModVersionTooLow(v)
} else {
msg, version = getVersionWhenGoModVersionSupported(v)
}

return getVersionWhenGoModVersionSupported(v)
// Make sure that we return a normal version string, not one starting with "v"
version = util.UnformatSemVer(version)
return
}

// Output some JSON to stdout specifying the version of Go to install, unless `version` is the
Expand Down
18 changes: 17 additions & 1 deletion go/extractor/autobuilder/build-environment_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package autobuilder

import "testing"
import (
"testing"

"github.com/github/codeql-go/extractor/util"
)

func TestGetVersionToInstall(t *testing.T) {
tests := map[versionInfo]string{
Expand Down Expand Up @@ -44,5 +48,17 @@ func TestGetVersionToInstall(t *testing.T) {
if actual != expected {
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
}

if input.goEnvVersionFound {
input.goEnvVersion = util.FormatSemVer(input.goEnvVersion)
}
if input.goEnvVersionFound {
input.goModVersion = util.FormatSemVer(input.goModVersion)
}

_, actual = getVersionToInstall(input)
if actual != expected {
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
}
}
}
6 changes: 3 additions & 3 deletions go/extractor/cli/go-autobuilder/go-autobuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,12 +537,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 semver.Compare(toolchain.GetEnvGoSemVer(), "v1.21.0") < 0 && greatestGoVersion.Found && semver.Compare(greatestGoVersion.Version, toolchain.GetEnvGoSemVer()) > 0 {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer(), greatestGoVersion.Version)
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.Version,
toolchain.GetEnvGoSemVer())
}
}
Expand Down
19 changes: 9 additions & 10 deletions go/extractor/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ type GoWorkspace struct {
Extracted bool // A value indicating whether this workspace was extracted successfully
}

// Represents a nullable version string.
// Represents a nullable version string. Use `VersionNotFound` and `VersionFound`
// instead of constructing values of this type directly.
type GoVersionInfo struct {
// The version string, if any
// The semantic version string, such as "v1.20.0-rc1", if any.
// This is a valid semantic version if `Found` is `true` or the empty string if not.
Version string
// A value indicating whether a version string was found
// A value indicating whether a version string was found.
// If this value is `true`, then `Version` is a valid semantic version.
// IF this value is `false`, then `Version` is the empty string.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// IF this value is `false`, then `Version` is the empty string.
// If this value is `false`, then `Version` is the empty string.

Found bool
}

Expand All @@ -73,13 +77,8 @@ var VersionNotFound GoVersionInfo = GoVersionInfo{"", false}

// Constructs a `GoVersionInfo` for a version we found.
func VersionFound(version string) GoVersionInfo {
// Add the "v" required by `semver` if there isn't one yet.
if !strings.HasPrefix(version, "v") {
version = "v" + version
}

return GoVersionInfo{
Version: version,
Version: util.FormatSemVer(version),
Found: true,
}
}
Expand Down Expand Up @@ -123,7 +122,7 @@ func RequiredGoVersion(workspaces *[]GoWorkspace) GoVersionInfo {
greatestGoVersion := VersionNotFound
for _, workspace := range *workspaces {
goVersionInfo := workspace.RequiredGoVersion()
if goVersionInfo.Found && (!greatestGoVersion.Found || semver.Compare("v"+goVersionInfo.Version, "v"+greatestGoVersion.Version) > 0) {
if goVersionInfo.Found && (!greatestGoVersion.Found || semver.Compare(util.FormatSemVer(goVersionInfo.Version), util.FormatSemVer(greatestGoVersion.Version)) > 0) {
greatestGoVersion = goVersionInfo
}
}
Expand Down
111 changes: 75 additions & 36 deletions go/extractor/project/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,63 +77,102 @@ func parseWorkFile(t *testing.T, contents string) *modfile.WorkFile {
return workFile
}

func TestRequiredGoVersion(t *testing.T) {
type ModVersionPair struct {
FileContents string
ExpectedVersion string
type FileVersionPair struct {
FileContents string
ExpectedVersion string
}

func checkRequiredGoVersionResult(t *testing.T, fun string, file string, testData FileVersionPair, result GoVersionInfo) {
if !result.Found {
t.Errorf(
"Expected %s to return %s for the below `%s` file, but got nothing:\n%s",
fun,
testData.ExpectedVersion,
file,
testData.FileContents,
)
} else if result.Version != testData.ExpectedVersion {
t.Errorf(
"Expected %s to return %s for the below `%s` file, but got %s:\n%s",
fun,
testData.ExpectedVersion,
file,
result.Version,
testData.FileContents,
)
}
}

modules := []ModVersionPair{
func TestRequiredGoVersion(t *testing.T) {
testFiles := []FileVersionPair{
{"go 1.20", "v1.20.0"},
{"go 1.21.2", "v1.21.2"},
{"go 1.21rc1", "v1.21.0-rc1"},
{"go 1.21rc1\ntoolchain go1.22.0", "v1.22.0"},
{"go 1.21rc1\ntoolchain go1.22rc1", "v1.22.0-rc1"},
}

for _, testData := range modules {
var modules []*GoModule = []*GoModule{}

for _, testData := range testFiles {
// `go.mod` and `go.work` files have mostly the same format
modFile := parseModFile(t, testData.FileContents)
workFile := parseWorkFile(t, testData.FileContents)
mod := GoModule{
mod := &GoModule{
Path: "test", // irrelevant
Module: modFile,
}
work := GoWorkspace{
work := &GoWorkspace{
WorkspaceFile: workFile,
}

result := mod.RequiredGoVersion()
if !result.Found {
t.Errorf(
"Expected mod.RequiredGoVersion() to return %s for the below `go.mod` file, but got nothing:\n%s",
testData.ExpectedVersion,
testData.FileContents,
)
} else if result.Version != testData.ExpectedVersion {
t.Errorf(
"Expected mod.RequiredGoVersion() to return %s for the below `go.mod` file, but got %s:\n%s",
testData.ExpectedVersion,
result.Version,
testData.FileContents,
)
}
checkRequiredGoVersionResult(t, "mod.RequiredGoVersion()", "go.mod", testData, result)

result = work.RequiredGoVersion()
if !result.Found {
t.Errorf(
"Expected mod.RequiredGoVersion() to return %s for the below `go.work` file, but got nothing:\n%s",
testData.ExpectedVersion,
testData.FileContents,
)
} else if result.Version != testData.ExpectedVersion {
t.Errorf(
"Expected mod.RequiredGoVersion() to return %s for the below `go.work` file, but got %s:\n%s",
testData.ExpectedVersion,
result.Version,
testData.FileContents,
)
}
checkRequiredGoVersionResult(t, "work.RequiredGoVersion()", "go.work", testData, result)

modules = append(modules, mod)
}

// Create a test workspace with all the modules in one workspace.
workspace := GoWorkspace{
Modules: modules,
}
workspaceVer := "v1.22.0"

result := RequiredGoVersion(&[]GoWorkspace{workspace})
if !result.Found {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got nothing.",
workspaceVer,
)
} else if result.Version != workspaceVer {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got %s.",
workspaceVer,
result.Version,
)
}

// Create test workspaces for each module.
workspaces := []GoWorkspace{}

for _, mod := range modules {
workspaces = append(workspaces, GoWorkspace{Modules: []*GoModule{mod}})
}

result = RequiredGoVersion(&workspaces)
if !result.Found {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got nothing.",
workspaceVer,
)
} else if result.Version != workspaceVer {
t.Errorf(
"Expected RequiredGoVersion to return %s, but got %s.",
workspaceVer,
result.Version,
)
}
}
1 change: 1 addition & 0 deletions go/extractor/toolchain/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions go/extractor/toolchain/toolchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ var goVersions = map[string]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{}{}
goVersions[semver.Canonical(util.FormatSemVer(version))] = struct{}{}
}

// Returns the current Go version as returned by 'go version', e.g. go1.14.4
Expand Down Expand Up @@ -58,7 +58,7 @@ func GetEnvGoVersion() string {

// Determines whether, to our knowledge, `version` is available on the current system.
func HasGoVersion(version string) bool {
_, found := goVersions[semver.Canonical("v"+version)]
_, found := goVersions[semver.Canonical(util.FormatSemVer(version))]
return found
}

Expand All @@ -72,7 +72,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" + semver.Canonical(util.FormatSemVer(version))[1:]
versionCmd := Version()
versionCmd.Dir = workingDir
versionCmd.Env = append(os.Environ(), toolchainArg)
Expand Down Expand Up @@ -104,20 +104,22 @@ func InstallVersion(workingDir string, version string) bool {
return true
}

// Converts a Go version to a semantic version.
// Converts a Go version to a semantic version. For example, "1.20" becomes "v1.20" and
// "1.22rc1" becomes "v1.22.0-rc1".
func GoVersionToSemVer(goVersion string) string {
// 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(goVersion, "rc")
if rcIndex != -1 {
return semver.Canonical("v"+goVersion[:rcIndex]) + "-" + goVersion[rcIndex:]
return semver.Canonical(util.FormatSemVer(goVersion[:rcIndex])) + "-" + goVersion[rcIndex:]
} else {
return semver.Canonical("v" + goVersion)
return semver.Canonical(util.FormatSemVer(goVersion))
}
}

// Converts a Go toolchain version of the form "go1.2.3" to a semantic version.
// Converts a Go toolchain version to a semantic version. For example, "go1.2.3" becomes "v1.2.3"
// and "go1.20rc1" becomes "v1.20.0-rc1".
func ToolchainVersionToSemVer(toolchainVersion string) string {
if !strings.HasPrefix(toolchainVersion, "go") {
log.Fatalf("Expected Go toolchain version of the form 'go1.2.3'; got '%s'", toolchainVersion)
Expand Down