Skip to content

Commit 73f71d8

Browse files
authored
Merge pull request #15979 from github/mbg/go/deal-with-incorrect-versions
2 parents aeacfb8 + f6c22d4 commit 73f71d8

File tree

14 files changed

+231
-12
lines changed

14 files changed

+231
-12
lines changed

go/extractor/cli/go-autobuilder/go-autobuilder.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ func installDependencies(workspace project.GoWorkspace) {
417417
} else {
418418
if workspace.Modules == nil {
419419
project.InitGoModForLegacyProject(workspace.BaseDir)
420-
workspace.Modules = project.LoadGoModules([]string{filepath.Join(workspace.BaseDir, "go.mod")})
420+
workspace.Modules = project.LoadGoModules(true, []string{filepath.Join(workspace.BaseDir, "go.mod")})
421421
}
422422

423423
// get dependencies for all modules

go/extractor/diagnostics/diagnostics.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ func EmitNewerSystemGoRequired(requiredVersion string) {
497497
func EmitExtractionFailedForProjects(path []string) {
498498
emitDiagnostic(
499499
"go/autobuilder/extraction-failed-for-project",
500-
fmt.Sprintf("Unable to extract %d Go projects", len(path)),
500+
"Unable to extract some Go projects",
501501
fmt.Sprintf(
502502
"The following %d Go project%s could not be extracted successfully:\n\n`%s`\n",
503503
len(path),
@@ -508,3 +508,18 @@ func EmitExtractionFailedForProjects(path []string) {
508508
noLocation,
509509
)
510510
}
511+
512+
func EmitInvalidToolchainVersion(goModPath string, version string) {
513+
emitDiagnostic(
514+
"go/autobuilder/invalid-go-toolchain-version",
515+
"Invalid Go toolchain version",
516+
strings.Join([]string{
517+
"As of Go 1.21, toolchain versions [must use the 1.N.P syntax](https://go.dev/doc/toolchain#version).",
518+
fmt.Sprintf("`%s` in `%s` does not match this syntax and there is no additional `toolchain` directive, which may cause some `go` commands to fail.", version, goModPath),
519+
},
520+
"\n\n"),
521+
severityWarning,
522+
fullVisibility,
523+
&locationStruct{File: goModPath},
524+
)
525+
}

go/extractor/project/project.go

+30-8
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,13 @@ func findGoModFiles(root string) []string {
176176
return util.FindAllFilesWithName(root, "go.mod", "vendor")
177177
}
178178

179+
// A regular expression for the Go toolchain version syntax.
180+
var toolchainVersionRe *regexp.Regexp = regexp.MustCompile(`(?m)^([0-9]+\.[0-9]+\.[0-9]+)$`)
181+
179182
// Given a list of `go.mod` file paths, try to parse them all. The resulting array of `GoModule` objects
180183
// will be the same length as the input array and the objects will contain at least the `go.mod` path.
181184
// If parsing the corresponding file is successful, then the parsed contents will also be available.
182-
func LoadGoModules(goModFilePaths []string) []*GoModule {
185+
func LoadGoModules(emitDiagnostics bool, goModFilePaths []string) []*GoModule {
183186
results := make([]*GoModule, len(goModFilePaths))
184187

185188
for i, goModFilePath := range goModFilePaths {
@@ -201,6 +204,25 @@ func LoadGoModules(goModFilePaths []string) []*GoModule {
201204
}
202205

203206
results[i].Module = modFile
207+
208+
// If this `go.mod` file specifies a Go language version, that version is `1.21` or greater, and
209+
// there is no `toolchain` directive, check that it is a valid Go toolchain version. Otherwise,
210+
// `go` commands which try to download the right version of the Go toolchain will fail. We detect
211+
// this situation and emit a diagnostic.
212+
if modFile.Toolchain == nil && modFile.Go != nil &&
213+
!toolchainVersionRe.Match([]byte(modFile.Go.Version)) && semver.Compare("v"+modFile.Go.Version, "v1.21.0") >= 0 {
214+
diagnostics.EmitInvalidToolchainVersion(goModFilePath, modFile.Go.Version)
215+
216+
modPath := filepath.Dir(goModFilePath)
217+
218+
log.Printf(
219+
"`%s` is not a valid toolchain version, trying to install it explicitly using the canonical representation in `%s`.",
220+
modFile.Go.Version,
221+
modPath,
222+
)
223+
224+
toolchain.InstallVersion(modPath, modFile.Go.Version)
225+
}
204226
}
205227

206228
return results
@@ -209,7 +231,7 @@ func LoadGoModules(goModFilePaths []string) []*GoModule {
209231
// Given a path to a `go.work` file, this function attempts to parse the `go.work` file. If unsuccessful,
210232
// we attempt to discover `go.mod` files within subdirectories of the directory containing the `go.work`
211233
// file ourselves.
212-
func discoverWorkspace(workFilePath string) GoWorkspace {
234+
func discoverWorkspace(emitDiagnostics bool, workFilePath string) GoWorkspace {
213235
log.Printf("Loading %s...\n", workFilePath)
214236
baseDir := filepath.Dir(workFilePath)
215237
workFileSrc, err := os.ReadFile(workFilePath)
@@ -223,7 +245,7 @@ func discoverWorkspace(workFilePath string) GoWorkspace {
223245

224246
return GoWorkspace{
225247
BaseDir: baseDir,
226-
Modules: LoadGoModules(goModFilePaths),
248+
Modules: LoadGoModules(emitDiagnostics, goModFilePaths),
227249
DepMode: GoGetWithModules,
228250
ModMode: getModMode(GoGetWithModules, baseDir),
229251
}
@@ -240,7 +262,7 @@ func discoverWorkspace(workFilePath string) GoWorkspace {
240262

241263
return GoWorkspace{
242264
BaseDir: baseDir,
243-
Modules: LoadGoModules(goModFilePaths),
265+
Modules: LoadGoModules(emitDiagnostics, goModFilePaths),
244266
DepMode: GoGetWithModules,
245267
ModMode: getModMode(GoGetWithModules, baseDir),
246268
}
@@ -263,7 +285,7 @@ func discoverWorkspace(workFilePath string) GoWorkspace {
263285
return GoWorkspace{
264286
BaseDir: baseDir,
265287
WorkspaceFile: workFile,
266-
Modules: LoadGoModules(goModFilePaths),
288+
Modules: LoadGoModules(emitDiagnostics, goModFilePaths),
267289
DepMode: GoGetWithModules,
268290
ModMode: ModReadonly, // Workspaces only support "readonly"
269291
}
@@ -286,7 +308,7 @@ func discoverWorkspaces(emitDiagnostics bool) []GoWorkspace {
286308
for i, goModFile := range goModFiles {
287309
results[i] = GoWorkspace{
288310
BaseDir: filepath.Dir(goModFile),
289-
Modules: LoadGoModules([]string{goModFile}),
311+
Modules: LoadGoModules(emitDiagnostics, []string{goModFile}),
290312
DepMode: GoGetWithModules,
291313
ModMode: getModMode(GoGetWithModules, filepath.Dir(goModFile)),
292314
}
@@ -303,7 +325,7 @@ func discoverWorkspaces(emitDiagnostics bool) []GoWorkspace {
303325

304326
results := make([]GoWorkspace, len(goWorkFiles))
305327
for i, workFilePath := range goWorkFiles {
306-
results[i] = discoverWorkspace(workFilePath)
328+
results[i] = discoverWorkspace(emitDiagnostics, workFilePath)
307329
}
308330

309331
// Add all stray `go.mod` files (i.e. those not referenced by `go.work` files)
@@ -335,7 +357,7 @@ func discoverWorkspaces(emitDiagnostics bool) []GoWorkspace {
335357
log.Printf("Module %s is not referenced by any go.work file; adding it separately.\n", goModFile)
336358
results = append(results, GoWorkspace{
337359
BaseDir: filepath.Dir(goModFile),
338-
Modules: LoadGoModules([]string{goModFile}),
360+
Modules: LoadGoModules(emitDiagnostics, []string{goModFile}),
339361
DepMode: GoGetWithModules,
340362
ModMode: getModMode(GoGetWithModules, filepath.Dir(goModFile)),
341363
})

go/extractor/toolchain/toolchain.go

+64-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ func IsInstalled() bool {
1818
return err == nil
1919
}
2020

21+
// The default Go version that is available on a system and a set of all versions
22+
// that we know are installed on the system.
2123
var goVersion = ""
24+
var goVersions = map[string]struct{}{}
25+
26+
// Adds an entry to the set of installed Go versions for the normalised `version` number.
27+
func addGoVersion(version string) {
28+
goVersions[semver.Canonical("v"+version)] = struct{}{}
29+
}
2230

2331
// Returns the current Go version as returned by 'go version', e.g. go1.14.4
2432
func GetEnvGoVersion() string {
@@ -27,7 +35,7 @@ func GetEnvGoVersion() string {
2735
// download the version of Go specified in there. That may either fail or result in us just
2836
// being told what's already in 'go.mod'. Setting 'GOTOOLCHAIN' to 'local' will force it
2937
// to use the local Go toolchain instead.
30-
cmd := exec.Command("go", "version")
38+
cmd := Version()
3139
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
3240
out, err := cmd.CombinedOutput()
3341

@@ -36,10 +44,59 @@ func GetEnvGoVersion() string {
3644
}
3745

3846
goVersion = parseGoVersion(string(out))
47+
addGoVersion(goVersion[2:])
3948
}
4049
return goVersion
4150
}
4251

52+
// Determines whether, to our knowledge, `version` is available on the current system.
53+
func HasGoVersion(version string) bool {
54+
_, found := goVersions[semver.Canonical("v"+version)]
55+
return found
56+
}
57+
58+
// Attempts to install the Go toolchain `version`.
59+
func InstallVersion(workingDir string, version string) bool {
60+
// No need to install it if we know that it is already installed.
61+
if HasGoVersion(version) {
62+
return true
63+
}
64+
65+
// Construct a command to invoke `go version` with `GOTOOLCHAIN=go1.N.0` to give
66+
// Go a valid toolchain version to download the toolchain we need; subsequent commands
67+
// should then work even with an invalid version that's still in `go.mod`
68+
toolchainArg := "GOTOOLCHAIN=go" + semver.Canonical("v" + version)[1:]
69+
versionCmd := Version()
70+
versionCmd.Dir = workingDir
71+
versionCmd.Env = append(os.Environ(), toolchainArg)
72+
versionCmd.Stdout = os.Stdout
73+
versionCmd.Stderr = os.Stderr
74+
75+
log.Printf(
76+
"Trying to install Go %s using its canonical representation in `%s`.",
77+
version,
78+
workingDir,
79+
)
80+
81+
// Run the command. If something goes wrong, report it to the log and signal failure
82+
// to the caller.
83+
if versionErr := versionCmd.Run(); versionErr != nil {
84+
log.Printf(
85+
"Failed to invoke `%s go version` in %s: %s\n",
86+
toolchainArg,
87+
versionCmd.Dir,
88+
versionErr.Error(),
89+
)
90+
91+
return false
92+
}
93+
94+
// Add the version to the set of versions that we know are installed and signal
95+
// success to the caller.
96+
addGoVersion(version)
97+
return true
98+
}
99+
43100
// Returns the current Go version in semver format, e.g. v1.14.4
44101
func GetEnvGoSemVer() string {
45102
goVersion := GetEnvGoVersion()
@@ -107,3 +164,9 @@ func VendorModule(path string) *exec.Cmd {
107164
modVendor.Dir = path
108165
return modVendor
109166
}
167+
168+
// Constructs a command to run `go version`.
169+
func Version() *exec.Cmd {
170+
version := exec.Command("go", "version")
171+
return version
172+
}

go/extractor/toolchain/toolchain_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,9 @@ func TestParseGoVersion(t *testing.T) {
1414
}
1515
}
1616
}
17+
18+
func TestHasGoVersion(t *testing.T) {
19+
if HasGoVersion("1.21") {
20+
t.Error("Expected HasGoVersion(\"1.21\") to be false, but got true")
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"location": {
3+
"file": "go.mod"
4+
},
5+
"markdownMessage": "As of Go 1.21, toolchain versions [must use the 1.N.P syntax](https://go.dev/doc/toolchain#version).\n\n`1.21` in `go.mod` does not match this syntax and there is no additional `toolchain` directive, which may cause some `go` commands to fail.",
6+
"severity": "warning",
7+
"source": {
8+
"extractorName": "go",
9+
"id": "go/autobuilder/invalid-go-toolchain-version",
10+
"name": "Invalid Go toolchain version"
11+
},
12+
"visibility": {
13+
"cliSummaryTable": true,
14+
"statusPage": true,
15+
"telemetry": true
16+
}
17+
}
18+
{
19+
"markdownMessage": "A single `go.mod` file was found.\n\n`go.mod`",
20+
"severity": "note",
21+
"source": {
22+
"extractorName": "go",
23+
"id": "go/autobuilder/single-root-go-mod-found",
24+
"name": "A single `go.mod` file was found in the root"
25+
},
26+
"visibility": {
27+
"cliSummaryTable": false,
28+
"statusPage": false,
29+
"telemetry": true
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go 1.21
2+
3+
module example
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package main
2+
3+
func main() {
4+
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
import subprocess
3+
4+
from create_database_utils import *
5+
from diagnostics_test_utils import *
6+
7+
# Set up a GOPATH relative to this test's root directory;
8+
# we set os.environ instead of using extra_env because we
9+
# need it to be set for the call to "go clean -modcache" later
10+
goPath = os.path.join(os.path.abspath(os.getcwd()), ".go")
11+
os.environ['GOPATH'] = goPath
12+
os.environ['LGTM_INDEX_IMPORT_PATH'] = "test"
13+
run_codeql_database_create([], lang="go", source="src")
14+
15+
check_diagnostics()
16+
17+
# Clean up the temporary GOPATH to prevent Bazel failures next
18+
# time the tests are run; see https://github.com/golang/go/issues/27161
19+
subprocess.call(["go", "clean", "-modcache"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"location": {
3+
"file": "go.mod"
4+
},
5+
"markdownMessage": "As of Go 1.21, toolchain versions [must use the 1.N.P syntax](https://go.dev/doc/toolchain#version).\n\n`1.21` in `go.mod` does not match this syntax and there is no additional `toolchain` directive, which may cause some `go` commands to fail.",
6+
"severity": "warning",
7+
"source": {
8+
"extractorName": "go",
9+
"id": "go/autobuilder/invalid-go-toolchain-version",
10+
"name": "Invalid Go toolchain version"
11+
},
12+
"visibility": {
13+
"cliSummaryTable": true,
14+
"statusPage": true,
15+
"telemetry": true
16+
}
17+
}
18+
{
19+
"markdownMessage": "A single `go.mod` file was found.\n\n`go.mod`",
20+
"severity": "note",
21+
"source": {
22+
"extractorName": "go",
23+
"id": "go/autobuilder/single-root-go-mod-found",
24+
"name": "A single `go.mod` file was found in the root"
25+
},
26+
"visibility": {
27+
"cliSummaryTable": false,
28+
"statusPage": false,
29+
"telemetry": true
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go 1.21
2+
3+
module test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package main
2+
3+
func main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import os
2+
import subprocess
3+
4+
from create_database_utils import *
5+
from diagnostics_test_utils import *
6+
7+
# Set up a GOPATH relative to this test's root directory;
8+
# we set os.environ instead of using extra_env because we
9+
# need it to be set for the call to "go clean -modcache" later
10+
goPath = os.path.join(os.path.abspath(os.getcwd()), ".go")
11+
os.environ['GOPATH'] = goPath
12+
run_codeql_database_create([], lang="go", source="src")
13+
14+
check_diagnostics()
15+
16+
# Clean up the temporary GOPATH to prevent Bazel failures next
17+
# time the tests are run; see https://github.com/golang/go/issues/27161
18+
subprocess.call(["go", "clean", "-modcache"])

go/ql/integration-tests/all-platforms/go/two-go-mods-one-failure/diagnostics.expected

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"source": {
1919
"extractorName": "go",
2020
"id": "go/autobuilder/extraction-failed-for-project",
21-
"name": "Unable to extract 1 Go projects"
21+
"name": "Unable to extract some Go projects"
2222
},
2323
"visibility": {
2424
"cliSummaryTable": true,

0 commit comments

Comments
 (0)