all: use the previous major Go release to bootstrap the build
To avoid having to maintain GO_BOOTSTRAP_VERSION in the playground
Dockerfile, always use the latest "published" minor of the previous Go
release as the bootstrap version, which per golang/go#54265 should
always be a sufficiently recent bootstrap version.
Here "published" means that the toolchain must exist, since it will be
downloaded for bootstrap. To enable this, add a `-toolchain` flag to the
latestgo command, which selects versions from the set of published
toolchains, rather than from Gerrit tags.
Fixes golang/go#69238
Change-Id: Ib4d4d7f2c0d5c4fbdccfec5d8bb83c040e0c5384
Reviewed-on: https://go-review.googlesource.com/c/playground/+/610675
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/Dockerfile b/Dockerfile
index 4d26bff..e5e18f6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,14 @@
# version of Go. See the configuration in the deploy directory.
ARG GO_VERSION=go1.22.6
+# GO_BOOTSTRAP_VERSION is downloaded below and used to bootstrap the build from
+# source. Therefore, this should be a version that is guaranteed to have
+# published artifacts, such as the latest minor of the previous major Go
+# release.
+#
+# See also https://go.dev/issue/69238.
+ARG GO_BOOSTRAP_VERSION=go1.22.6
+
############################################################################
# Build Go at GO_VERSION, and build faketime standard library.
FROM debian:buster AS build-go
@@ -22,9 +30,12 @@
ENV GOPATH /go
ENV GOROOT_BOOTSTRAP=/usr/local/go-bootstrap
-ENV GO_BOOTSTRAP_VERSION go1.22.6
+
+# https://docs.docker.com/reference/dockerfile/#understand-how-arg-and-from-interact
ARG GO_VERSION
ENV GO_VERSION ${GO_VERSION}
+ARG GO_BOOTSTRAP_VERSION
+ENV GO_BOOTSTRAP_VERSION ${GO_BOOTSTRAP_VERSION}
# Get a bootstrap version of Go for building GO_VERSION. At the time
# of this Dockerfile being built, GO_VERSION's artifacts may not yet
diff --git a/cmd/latestgo/main.go b/cmd/latestgo/main.go
index d313fde..1b4f67b 100644
--- a/cmd/latestgo/main.go
+++ b/cmd/latestgo/main.go
@@ -7,9 +7,12 @@
import (
"context"
+ "encoding/json"
"flag"
"fmt"
+ "io"
"log"
+ "net/http"
"sort"
"strings"
"time"
@@ -18,16 +21,38 @@
"golang.org/x/build/maintner/maintnerd/maintapi/version"
)
-var prev = flag.Bool("prev", false, "whether to query the previous Go release, rather than the last (e.g. 1.17 versus 1.18)")
+var (
+ prev = flag.Bool("prev", false, "if set, query the previous Go release rather than the last (e.g. 1.17 versus 1.18)")
+ toolchain = flag.Bool("toolchain", false, "if set, query released toolchains, rather than gerrit tags; toolchains may lag behind gerrit")
+)
func main() {
- client := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
-
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
+ var latest []string
+ if *toolchain {
+ latest = latestToolchainVersions(ctx)
+ } else {
+ client := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
+ latest = latestGerritVersions(ctx, client)
+ }
+ if len(latest) < 2 {
+ log.Fatalf("found %d versions, need at least 2", len(latest))
+ }
+
+ if *prev {
+ fmt.Println(latest[1])
+ } else {
+ fmt.Println(latest[0])
+ }
+}
+
+// latestGerritVersions queries the latest versions for each major Go release,
+// among Gerrit tags.
+func latestGerritVersions(ctx context.Context, client *gerrit.Client) []string {
tagInfo, err := client.GetProjectTags(ctx, "go")
if err != nil {
log.Fatalf("error retrieving project tags for 'go': %v", err)
@@ -37,6 +62,54 @@
log.Fatalln("no project tags found for 'go'")
}
+ var tags []string
+ for _, tag := range tagInfo {
+ tags = append(tags, strings.TrimPrefix(tag.Ref, "refs/tags/"))
+ }
+ return latestPatches(tags)
+}
+
+// latestToolchainVersions queries the latest versions for each major Go
+// release, among published toolchains. It may have fewer versions than
+// [latestGerritVersions], because not all toolchains may be published.
+func latestToolchainVersions(ctx context.Context) []string {
+ req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/dl/?mode=json", nil)
+ if err != nil {
+ log.Fatalf("NewRequest: %v", err)
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ log.Fatalf("fetching toolchains: %v", err)
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ log.Fatalf("fetching toolchains: got status %d, want 200", res.StatusCode)
+ }
+ data, err := io.ReadAll(res.Body)
+ if err != nil {
+ log.Fatalf("reading body: %v", err)
+ }
+
+ type release struct {
+ Version string `json:"version"`
+ }
+ var releases []release
+ if err := json.Unmarshal(data, &releases); err != nil {
+ log.Fatalf("unmarshaling releases JSON: %v", err)
+ }
+ var all []string
+ for _, rel := range releases {
+ all = append(all, rel.Version)
+ }
+ return latestPatches(all)
+}
+
+// latestPatches returns the latest minor release of each major Go version,
+// among the set of tag or tag-like strings. The result is in descending
+// order, such that later versions are sorted first.
+//
+// Tags that aren't of the form goX, goX.Y, or goX.Y.Z are ignored.
+func latestPatches(tags []string) []string {
// Find the latest patch version for each major Go version.
type majMin struct {
maj, min int // maj, min in semver terminology, which corresponds to a major go release
@@ -47,16 +120,14 @@
}
latestPatches := make(map[majMin]patchTag) // (maj, min) -> latest patch info
- for _, tag := range tagInfo {
- tagName := strings.TrimPrefix(tag.Ref, "refs/tags/")
- maj, min, patch, ok := version.ParseTag(tagName)
+ for _, tag := range tags {
+ maj, min, patch, ok := version.ParseTag(tag)
if !ok {
continue
}
-
mm := majMin{maj, min}
if latest, ok := latestPatches[mm]; !ok || latest.patch < patch {
- latestPatches[mm] = patchTag{patch, tagName}
+ latestPatches[mm] = patchTag{patch, tag}
}
}
@@ -64,18 +135,17 @@
for mm := range latestPatches {
mms = append(mms, mm)
}
+ // Sort by descending semantic ordering, so that later versions are first.
sort.Slice(mms, func(i, j int) bool {
if mms[i].maj != mms[j].maj {
- return mms[i].maj < mms[j].maj
+ return mms[i].maj > mms[j].maj
}
- return mms[i].min < mms[j].min
+ return mms[i].min > mms[j].min
})
- var mm majMin
- if *prev && len(mms) > 1 {
- mm = mms[len(mms)-2] // latest patch of the previous Go release
- } else {
- mm = mms[len(mms)-1]
+ var latest []string
+ for _, mm := range mms {
+ latest = append(latest, latestPatches[mm].tag)
}
- fmt.Print(latestPatches[mm].tag)
+ return latest
}
diff --git a/deploy/deploy.json b/deploy/deploy.json
index 43e4247..db7ca68 100644
--- a/deploy/deploy.json
+++ b/deploy/deploy.json
@@ -9,11 +9,19 @@
]
},
{
+ "name": "golang",
+ "entrypoint": "sh",
+ "args": [
+ "-c",
+ "go run golang.org/x/playground/cmd/latestgo -prev -toolchain > /workspace/gobootstrapversion && echo GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion`"
+ ]
+ },
+ {
"name": "gcr.io/cloud-builders/docker",
"entrypoint": "sh",
"args": [
"-c",
- "docker build --build-arg GO_VERSION=`cat /workspace/goversion` -t gcr.io/$PROJECT_ID/playground ."
+ "docker build --build-arg GO_VERSION=`cat /workspace/goversion` --build-arg GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion` -t gcr.io/$PROJECT_ID/playground ."
]
},
{
diff --git a/deploy/deploy_goprev.json b/deploy/deploy_goprev.json
index e97e180..8edfbd9 100644
--- a/deploy/deploy_goprev.json
+++ b/deploy/deploy_goprev.json
@@ -9,11 +9,19 @@
]
},
{
+ "name": "golang",
+ "entrypoint": "sh",
+ "args": [
+ "-c",
+ "go run golang.org/x/playground/cmd/latestgo -prev -toolchain > /workspace/gobootstrapversion && echo GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion`"
+ ]
+ },
+ {
"name": "gcr.io/cloud-builders/docker",
"entrypoint": "sh",
"args": [
"-c",
- "docker build --build-arg GO_VERSION=`cat /workspace/goversion` -t gcr.io/$PROJECT_ID/playground-goprev ."
+ "docker build --build-arg GO_VERSION=`cat /workspace/goversion` --build_arg GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion` -t gcr.io/$PROJECT_ID/playground-goprev ."
]
},
{
diff --git a/deploy/deploy_gotip.json b/deploy/deploy_gotip.json
index cafeb16..4eb8b17 100644
--- a/deploy/deploy_gotip.json
+++ b/deploy/deploy_gotip.json
@@ -1,11 +1,19 @@
{
"steps": [
{
+ "name": "golang",
+ "entrypoint": "sh",
+ "args": [
+ "-c",
+ "go run golang.org/x/playground/cmd/latestgo -prev -toolchain > /workspace/gobootstrapversion && echo GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion`"
+ ]
+ },
+ {
"name": "gcr.io/cloud-builders/docker",
"entrypoint": "sh",
"args": [
"-c",
- "docker build --build-arg GO_VERSION=master -t gcr.io/$PROJECT_ID/playground-gotip ."
+ "docker build --build-arg GO_VERSION=master --build-arg GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion` -t gcr.io/$PROJECT_ID/playground-gotip ."
]
},
{