Skip to content

Commit 514430e

Browse files
authored
Merge pull request #15411 from github/mbg/go/refactor-go-autobuilder
2 parents 3af42d5 + cf1aab0 commit 514430e

File tree

7 files changed

+657
-616
lines changed

7 files changed

+657
-616
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package autobuilder
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
8+
"github.com/github/codeql-go/extractor/diagnostics"
9+
"github.com/github/codeql-go/extractor/project"
10+
"github.com/github/codeql-go/extractor/toolchain"
11+
"golang.org/x/mod/semver"
12+
)
13+
14+
const minGoVersion = "1.11"
15+
const maxGoVersion = "1.21"
16+
17+
type versionInfo struct {
18+
goModVersion string // The version of Go found in the go directive in the `go.mod` file.
19+
goModVersionFound bool // Whether a `go` directive was found in the `go.mod` file.
20+
goEnvVersion string // The version of Go found in the environment.
21+
goEnvVersionFound bool // Whether an installation of Go was found in the environment.
22+
}
23+
24+
func (v versionInfo) String() string {
25+
return fmt.Sprintf(
26+
"go.mod version: %s, go.mod directive found: %t, go env version: %s, go installation found: %t",
27+
v.goModVersion, v.goModVersionFound, v.goEnvVersion, v.goEnvVersionFound)
28+
}
29+
30+
// Check if `version` is lower than `minGoVersion`. Note that for this comparison we ignore the
31+
// patch part of the version, so 1.20.1 and 1.20 are considered equal.
32+
func belowSupportedRange(version string) bool {
33+
return semver.Compare(semver.MajorMinor("v"+version), "v"+minGoVersion) < 0
34+
}
35+
36+
// Check if `version` is higher than `maxGoVersion`. Note that for this comparison we ignore the
37+
// patch part of the version, so 1.20.1 and 1.20 are considered equal.
38+
func aboveSupportedRange(version string) bool {
39+
return semver.Compare(semver.MajorMinor("v"+version), "v"+maxGoVersion) > 0
40+
}
41+
42+
// Check if `version` is lower than `minGoVersion` or higher than `maxGoVersion`. Note that for
43+
// this comparison we ignore the patch part of the version, so 1.20.1 and 1.20 are considered
44+
// equal.
45+
func outsideSupportedRange(version string) bool {
46+
return belowSupportedRange(version) || aboveSupportedRange(version)
47+
}
48+
49+
// Assuming `v.goModVersionFound` is false, emit a diagnostic and return the version to install,
50+
// or the empty string if we should not attempt to install a version of Go.
51+
func getVersionWhenGoModVersionNotFound(v versionInfo) (msg, version string) {
52+
if !v.goEnvVersionFound {
53+
// There is no Go version installed in the environment. We have no indication which version
54+
// was intended to be used to build this project. Go versions are generally backwards
55+
// compatible, so we install the maximum supported version.
56+
msg = "No version of Go installed and no `go.mod` file found. Requesting the maximum " +
57+
"supported version of Go (" + maxGoVersion + ")."
58+
version = maxGoVersion
59+
diagnostics.EmitNoGoModAndNoGoEnv(msg)
60+
} else if outsideSupportedRange(v.goEnvVersion) {
61+
// The Go version installed in the environment is not supported. We have no indication
62+
// which version was intended to be used to build this project. Go versions are generally
63+
// backwards compatible, so we install the maximum supported version.
64+
msg = "No `go.mod` file found. The version of Go installed in the environment (" +
65+
v.goEnvVersion + ") is outside of the supported range (" + minGoVersion + "-" +
66+
maxGoVersion + "). Requesting the maximum supported version of Go (" + maxGoVersion +
67+
")."
68+
version = maxGoVersion
69+
diagnostics.EmitNoGoModAndGoEnvUnsupported(msg)
70+
} else {
71+
// The version of Go that is installed is supported. We have no indication which version
72+
// was intended to be used to build this project. We assume that the installed version is
73+
// suitable and do not install a version of Go.
74+
msg = "No `go.mod` file found. Version " + v.goEnvVersion + " installed in the " +
75+
"environment is supported. Not requesting any version of Go."
76+
version = ""
77+
diagnostics.EmitNoGoModAndGoEnvSupported(msg)
78+
}
79+
80+
return msg, version
81+
}
82+
83+
// Assuming `v.goModVersion` is above the supported range, emit a diagnostic and return the
84+
// version to install, or the empty string if we should not attempt to install a version of Go.
85+
func getVersionWhenGoModVersionTooHigh(v versionInfo) (msg, version string) {
86+
if !v.goEnvVersionFound {
87+
// The version in the `go.mod` file is above the supported range. There is no Go version
88+
// installed. We install the maximum supported version as a best effort.
89+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
90+
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
91+
"). No version of Go installed. Requesting the maximum supported version of Go (" +
92+
maxGoVersion + ")."
93+
version = maxGoVersion
94+
diagnostics.EmitGoModVersionTooHighAndNoGoEnv(msg)
95+
} else if aboveSupportedRange(v.goEnvVersion) {
96+
// The version in the `go.mod` file is above the supported range. The version of Go that
97+
// is installed is above the supported range. We do not install a version of Go.
98+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
99+
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
100+
"). The version of Go installed in the environment (" + v.goEnvVersion +
101+
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
102+
"). Not requesting any version of Go."
103+
version = ""
104+
diagnostics.EmitGoModVersionTooHighAndEnvVersionTooHigh(msg)
105+
} else if belowSupportedRange(v.goEnvVersion) {
106+
// The version in the `go.mod` file is above the supported range. The version of Go that
107+
// is installed is below the supported range. We install the maximum supported version as
108+
// a best effort.
109+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
110+
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
111+
"). The version of Go installed in the environment (" + v.goEnvVersion +
112+
") is below the supported range (" + minGoVersion + "-" + maxGoVersion +
113+
"). Requesting the maximum supported version of Go (" + maxGoVersion + ")."
114+
version = maxGoVersion
115+
diagnostics.EmitGoModVersionTooHighAndEnvVersionTooLow(msg)
116+
} else if semver.Compare("v"+maxGoVersion, "v"+v.goEnvVersion) > 0 {
117+
// The version in the `go.mod` file is above the supported range. The version of Go that
118+
// is installed is supported and below the maximum supported version. We install the
119+
// maximum supported version as a best effort.
120+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
121+
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
122+
"). The version of Go installed in the environment (" + v.goEnvVersion +
123+
") is below the maximum supported version (" + maxGoVersion +
124+
"). Requesting the maximum supported version of Go (" + maxGoVersion + ")."
125+
version = maxGoVersion
126+
diagnostics.EmitGoModVersionTooHighAndEnvVersionBelowMax(msg)
127+
} else {
128+
// The version in the `go.mod` file is above the supported range. The version of Go that
129+
// is installed is the maximum supported version. We do not install a version of Go.
130+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
131+
") is above the supported range (" + minGoVersion + "-" + maxGoVersion +
132+
"). The version of Go installed in the environment (" + v.goEnvVersion +
133+
") is the maximum supported version (" + maxGoVersion +
134+
"). Not requesting any version of Go."
135+
version = ""
136+
diagnostics.EmitGoModVersionTooHighAndEnvVersionMax(msg)
137+
}
138+
139+
return msg, version
140+
}
141+
142+
// Assuming `v.goModVersion` is below the supported range, emit a diagnostic and return the
143+
// version to install, or the empty string if we should not attempt to install a version of Go.
144+
func getVersionWhenGoModVersionTooLow(v versionInfo) (msg, version string) {
145+
if !v.goEnvVersionFound {
146+
// There is no Go version installed. The version in the `go.mod` file is below the
147+
// supported range. Go versions are generally backwards compatible, so we install the
148+
// minimum supported version.
149+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
150+
") is below the supported range (" + minGoVersion + "-" + maxGoVersion +
151+
"). No version of Go installed. Requesting the minimum supported version of Go (" +
152+
minGoVersion + ")."
153+
version = minGoVersion
154+
diagnostics.EmitGoModVersionTooLowAndNoGoEnv(msg)
155+
} else if outsideSupportedRange(v.goEnvVersion) {
156+
// The version of Go that is installed is outside of the supported range. The version
157+
// in the `go.mod` file is below the supported range. Go versions are generally
158+
// backwards compatible, so we install the minimum supported version.
159+
msg = "The version of Go found in the `go.mod` file (" + v.goModVersion +
160+
") is below the supported range (" + minGoVersion + "-" + maxGoVersion +
161+
"). The version of Go installed in the environment (" + v.goEnvVersion +
162+
") is outside of the supported range (" + minGoVersion + "-" + maxGoVersion + "). " +
163+
"Requesting the minimum supported version of Go (" + minGoVersion + ")."
164+
version = minGoVersion
165+
diagnostics.EmitGoModVersionTooLowAndEnvVersionUnsupported(msg)
166+
} else {
167+
// The version of Go that is installed is supported. The version in the `go.mod` file is
168+
// below the supported range. We do not install a version of Go.
169+
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
170+
") is supported and is high enough for the version found in the `go.mod` file (" +
171+
v.goModVersion + "). Not requesting any version of Go."
172+
version = ""
173+
diagnostics.EmitGoModVersionTooLowAndEnvVersionSupported(msg)
174+
}
175+
176+
return msg, version
177+
}
178+
179+
// Assuming `v.goModVersion` is in the supported range, emit a diagnostic and return the version
180+
// to install, or the empty string if we should not attempt to install a version of Go.
181+
func getVersionWhenGoModVersionSupported(v versionInfo) (msg, version string) {
182+
if !v.goEnvVersionFound {
183+
// There is no Go version installed. The version in the `go.mod` file is supported.
184+
// We install the version from the `go.mod` file.
185+
msg = "No version of Go installed. Requesting the version of Go found in the `go.mod` " +
186+
"file (" + v.goModVersion + ")."
187+
version = v.goModVersion
188+
diagnostics.EmitGoModVersionSupportedAndNoGoEnv(msg)
189+
} else if outsideSupportedRange(v.goEnvVersion) {
190+
// The version of Go that is installed is outside of the supported range. The version in
191+
// the `go.mod` file is supported. We install the version from the `go.mod` file.
192+
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
193+
") is outside of the supported range (" + minGoVersion + "-" + maxGoVersion + "). " +
194+
"Requesting the version of Go from the `go.mod` file (" +
195+
v.goModVersion + ")."
196+
version = v.goModVersion
197+
diagnostics.EmitGoModVersionSupportedAndGoEnvUnsupported(msg)
198+
} else if semver.Compare("v"+v.goModVersion, "v"+v.goEnvVersion) > 0 {
199+
// The version of Go that is installed is supported. The version in the `go.mod` file is
200+
// supported and is higher than the version that is installed. We install the version from
201+
// the `go.mod` file.
202+
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
203+
") is lower than the version found in the `go.mod` file (" + v.goModVersion +
204+
"). Requesting the version of Go from the `go.mod` file (" + v.goModVersion + ")."
205+
version = v.goModVersion
206+
diagnostics.EmitGoModVersionSupportedHigherGoEnv(msg)
207+
} else {
208+
// The version of Go that is installed is supported. The version in the `go.mod` file is
209+
// supported and is lower than or equal to the version that is installed. We do not install
210+
// a version of Go.
211+
msg = "The version of Go installed in the environment (" + v.goEnvVersion +
212+
") is supported and is high enough for the version found in the `go.mod` file (" +
213+
v.goModVersion + "). Not requesting any version of Go."
214+
version = ""
215+
diagnostics.EmitGoModVersionSupportedLowerEqualGoEnv(msg)
216+
}
217+
218+
return msg, version
219+
}
220+
221+
// Check the versions of Go found in the environment and in the `go.mod` file, and return a
222+
// version to install. If the version is the empty string then no installation is required.
223+
// We never return a version of Go that is outside of the supported range.
224+
//
225+
// +-----------------------+-----------------------+-----------------------+-----------------------------------------------------+------------------------------------------------+
226+
// | Found in go.mod > | *None* | *Below min supported* | *In supported range* | *Above max supported |
227+
// | Installed \/ | | | | |
228+
// |-----------------------|-----------------------|-----------------------|-----------------------------------------------------|------------------------------------------------|
229+
// | *None* | Install max supported | Install min supported | Install version from go.mod | Install max supported |
230+
// | *Below min supported* | Install max supported | Install min supported | Install version from go.mod | Install max supported |
231+
// | *In supported range* | No action | No action | Install version from go.mod if newer than installed | Install max supported if newer than installed |
232+
// | *Above max supported* | Install max supported | Install min supported | Install version from go.mod | No action |
233+
// +-----------------------+-----------------------+-----------------------+-----------------------------------------------------+------------------------------------------------+
234+
func getVersionToInstall(v versionInfo) (msg, version string) {
235+
if !v.goModVersionFound {
236+
return getVersionWhenGoModVersionNotFound(v)
237+
}
238+
239+
if aboveSupportedRange(v.goModVersion) {
240+
return getVersionWhenGoModVersionTooHigh(v)
241+
}
242+
243+
if belowSupportedRange(v.goModVersion) {
244+
return getVersionWhenGoModVersionTooLow(v)
245+
}
246+
247+
return getVersionWhenGoModVersionSupported(v)
248+
}
249+
250+
// Output some JSON to stdout specifying the version of Go to install, unless `version` is the
251+
// empty string.
252+
func outputEnvironmentJson(version string) {
253+
var content string
254+
if version == "" {
255+
content = `{ "go": {} }`
256+
} else {
257+
content = `{ "go": { "version": "` + version + `" } }`
258+
}
259+
_, err := fmt.Fprint(os.Stdout, content)
260+
261+
if err != nil {
262+
log.Println("Failed to write environment json to stdout: ")
263+
log.Println(err)
264+
}
265+
}
266+
267+
// Get the version of Go to install and output it to stdout as json.
268+
func IdentifyEnvironment() {
269+
var v versionInfo
270+
buildInfo := project.GetBuildInfo(false)
271+
goVersionInfo := project.TryReadGoDirective(buildInfo)
272+
v.goModVersion, v.goModVersionFound = goVersionInfo.Version, goVersionInfo.Found
273+
274+
v.goEnvVersionFound = toolchain.IsInstalled()
275+
if v.goEnvVersionFound {
276+
v.goEnvVersion = toolchain.GetEnvGoVersion()[2:]
277+
}
278+
279+
msg, versionToInstall := getVersionToInstall(v)
280+
log.Println(msg)
281+
282+
outputEnvironmentJson(versionToInstall)
283+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package autobuilder
2+
3+
import "testing"
4+
5+
func TestGetVersionToInstall(t *testing.T) {
6+
tests := map[versionInfo]string{
7+
// getVersionWhenGoModVersionNotFound()
8+
{"", false, "", false}: maxGoVersion,
9+
{"", false, "1.2.2", true}: maxGoVersion,
10+
{"", false, "9999.0.1", true}: maxGoVersion,
11+
{"", false, "1.11.13", true}: "",
12+
{"", false, "1.20.3", true}: "",
13+
14+
// getVersionWhenGoModVersionTooHigh()
15+
{"9999.0", true, "", false}: maxGoVersion,
16+
{"9999.0", true, "9999.0.1", true}: "",
17+
{"9999.0", true, "1.1", true}: maxGoVersion,
18+
{"9999.0", true, minGoVersion, false}: maxGoVersion,
19+
{"9999.0", true, maxGoVersion, true}: "",
20+
21+
// getVersionWhenGoModVersionTooLow()
22+
{"0.0", true, "", false}: minGoVersion,
23+
{"0.0", true, "9999.0", true}: minGoVersion,
24+
{"0.0", true, "1.2.2", true}: minGoVersion,
25+
{"0.0", true, "1.20.3", true}: "",
26+
27+
// getVersionWhenGoModVersionSupported()
28+
{"1.20", true, "", false}: "1.20",
29+
{"1.11", true, "", false}: "1.11",
30+
{"1.20", true, "1.2.2", true}: "1.20",
31+
{"1.11", true, "1.2.2", true}: "1.11",
32+
{"1.20", true, "9999.0.1", true}: "1.20",
33+
{"1.11", true, "9999.0.1", true}: "1.11",
34+
// go.mod version > go installation version
35+
{"1.20", true, "1.11.13", true}: "1.20",
36+
{"1.20", true, "1.12", true}: "1.20",
37+
// go.mod version <= go installation version (Note comparisons ignore the patch version)
38+
{"1.11", true, "1.20", true}: "",
39+
{"1.11", true, "1.20.3", true}: "",
40+
{"1.20", true, "1.20.3", true}: "",
41+
}
42+
for input, expected := range tests {
43+
_, actual := getVersionToInstall(input)
44+
if actual != expected {
45+
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)