cmd/racebuild: improve reproducibility of .syso builds

Use a known version of MinGW on Windows. (On other platforms, we use
whatever compiler is already installed on the system image.)

Test at a well-defined Go commit.

Record the Go and LLVM commits for each platform independently.
Extracting the C++ toolchain version is left for future work, but it
should be recoverable from the resulting .syso file anyway.

Change-Id: I9af1d2a6f540a4d276b87074864564bf989e4731
Reviewed-on: https://go-review.googlesource.com/115375
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/racebuild/racebuild.go b/cmd/racebuild/racebuild.go
index 1c9b549..eaf474c 100644
--- a/cmd/racebuild/racebuild.go
+++ b/cmd/racebuild/racebuild.go
@@ -23,6 +23,7 @@
 	"path/filepath"
 	"regexp"
 	"strings"
+	"sync"
 
 	"golang.org/x/sync/errgroup"
 )
@@ -30,9 +31,13 @@
 var (
 	flagGoroot    = flag.String("goroot", "", "path to Go repository to update (required)")
 	flagRev       = flag.String("rev", "", "llvm compiler-rt git revision from http://llvm.org/git/compiler-rt.git (required)")
+	flagGoRev     = flag.String("gorev", "HEAD", "Go repository revision to use; HEAD is relative to --goroot")
 	flagPlatforms = flag.String("platforms", "all", `comma-separated platforms (such as "linux/amd64") to rebuild, or "all"`)
 )
 
+// goRev is the resolved commit ID of flagGoRev.
+var goRev string
+
 // TODO: use buildlet package instead of calling out to gomote.
 var platforms = []*Platform{
 	&Platform{
@@ -42,6 +47,9 @@
 		Script: `#!/usr/bin/env bash
 set -e
 git clone https://go.googlesource.com/go
+pushd go
+git checkout $GOREV
+popd
 git clone http://llvm.org/git/compiler-rt.git
 (cd compiler-rt && git checkout $REV)
 (cd compiler-rt/lib/tsan/go && CC=clang ./buildgo.sh)
@@ -56,6 +64,9 @@
 		Script: `#!/usr/bin/env bash
 set -e
 git clone https://go.googlesource.com/go
+pushd go
+git checkout $GOREV
+popd
 git clone http://llvm.org/git/compiler-rt.git
 (cd compiler-rt && git checkout $REV)
 (cd compiler-rt/lib/tsan/go && CC=clang ./buildgo.sh)
@@ -72,6 +83,9 @@
 apt-get update
 apt-get install -y git g++
 git clone https://go.googlesource.com/go
+pushd go
+git checkout $GOREV
+popd
 git clone http://llvm.org/git/compiler-rt.git
 (cd compiler-rt && git checkout $REV)
 (cd compiler-rt/lib/tsan/go && ./buildgo.sh)
@@ -88,6 +102,9 @@
 apt-get update
 apt-get install -y git g++
 git clone https://go.googlesource.com/go
+pushd go
+git checkout $GOREV
+popd
 git clone http://llvm.org/git/compiler-rt.git
 (cd compiler-rt && git checkout $REV)
 (cd compiler-rt/lib/tsan/go && ./buildgo.sh)
@@ -103,6 +120,9 @@
 		Script: `#!/usr/bin/env bash
 set -e
 git clone https://go.googlesource.com/go
+pushd go
+git checkout $GOREV
+popd
 git clone http://llvm.org/git/compiler-rt.git
 (cd compiler-rt && git checkout $REV)
 (cd compiler-rt/lib/tsan/go && CC=clang ./buildgo.sh)
@@ -119,11 +139,15 @@
 @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
 choco install git -y
 if %errorlevel% neq 0 exit /b %errorlevel%
-choco install mingw -y
+choco install mingw --version 5.3.0 -y
 if %errorlevel% neq 0 exit /b %errorlevel%
 call refreshenv
 git clone https://go.googlesource.com/go
 if %errorlevel% neq 0 exit /b %errorlevel%
+cd go
+git checkout %GOREV%
+if %errorlevel% neq 0 exit /b %errorlevel%
+cd ..
 git clone http://llvm.org/git/compiler-rt.git
 if %errorlevel% neq 0 exit /b %errorlevel%
 cd compiler-rt
@@ -189,27 +213,21 @@
 
 func main() {
 	flag.Parse()
-	if *flagRev == "" || *flagGoroot == "" {
+	if *flagRev == "" || *flagGoroot == "" || *flagGoRev == "" {
 		flag.PrintDefaults()
 		os.Exit(1)
 	}
 	parsePlatformsFlag()
 
-	// Update revision in the README file.
-	// Do this early to check goroot correctness.
-	readmeFile := filepath.Join(*flagGoroot, "src", "runtime", "race", "README")
-	readme, err := ioutil.ReadFile(readmeFile)
+	cmd := exec.Command("git", "rev-parse", *flagGoRev)
+	cmd.Dir = *flagGoroot
+	cmd.Stderr = os.Stderr
+	out, err := cmd.Output()
 	if err != nil {
-		log.Fatalf("bad -goroot? %v", err)
+		log.Fatal("%s failed: %v", strings.Join(cmd.Args, " "), err)
 	}
-	readmeRev := regexp.MustCompile("Current runtime is built on rev ([0-9,a-z]+)\\.").FindSubmatchIndex(readme)
-	if readmeRev == nil {
-		log.Fatalf("failed to find current revision in src/runtime/race/README")
-	}
-	readme = bytes.Replace(readme, readme[readmeRev[2]:readmeRev[3]], []byte(*flagRev), -1)
-	if err := ioutil.WriteFile(readmeFile, readme, 0640); err != nil {
-		log.Fatalf("failed to write README file: %v", err)
-	}
+	goRev = string(bytes.TrimSpace(out))
+	log.Printf("using Go revision: %s", goRev)
 
 	// Start build on all platforms in parallel.
 	// On interrupt, destroy any in-flight builders before exiting.
@@ -232,13 +250,12 @@
 			if err := p.Build(ctx); err != nil {
 				return fmt.Errorf("%v failed: %v", p.Name(), err)
 			}
-			return nil
+			return p.UpdateReadme()
 		})
 	}
 
 	if err := g.Wait(); err != nil {
-		log.Println(err)
-		os.Exit(1)
+		log.Fatal(err)
 	}
 }
 
@@ -254,6 +271,11 @@
 	return fmt.Sprintf("%v/%v", p.OS, p.Arch)
 }
 
+// Basename returns the name of the output file relative to src/runtime/race.
+func (p *Platform) Basename() string {
+	return fmt.Sprintf("race_%v_%s.syso", p.OS, p.Arch)
+}
+
 func (p *Platform) Build(ctx context.Context) error {
 	// Create gomote instance (or reuse an existing instance for debugging).
 	var lastErr error
@@ -304,12 +326,12 @@
 	if _, err := p.Gomote(ctx, "put", "-mode=0700", p.Inst, script.Name(), targetName); err != nil {
 		return err
 	}
-	if _, err := p.Gomote(ctx, "run", "-e=REV="+*flagRev, p.Inst, targetName); err != nil {
+	if _, err := p.Gomote(ctx, "run", "-e=REV="+*flagRev, "-e=GOREV="+goRev, p.Inst, targetName); err != nil {
 		return err
 	}
 
 	// The script is supposed to leave updated runtime at that path. Copy it out.
-	syso := fmt.Sprintf("race_%v_%s.syso", p.OS, p.Arch)
+	syso := p.Basename()
 	targz, err := p.Gomote(ctx, "gettar", "-dir=go/src/runtime/race/"+syso, p.Inst)
 	if err != nil {
 		return err
@@ -348,6 +370,41 @@
 	return nil
 }
 
+var readmeMu sync.Mutex
+
+func (p *Platform) UpdateReadme() error {
+	readmeMu.Lock()
+	defer readmeMu.Unlock()
+
+	readmeFile := filepath.Join(*flagGoroot, "src", "runtime", "race", "README")
+	readme, err := ioutil.ReadFile(readmeFile)
+	if err != nil {
+		log.Fatalf("bad -goroot? %v", err)
+	}
+
+	syso := p.Basename()
+	const (
+		readmeTmpl = "%s built with LLVM %s and Go %s."
+		commitRE   = "[0-9a-f]+"
+	)
+
+	// TODO(bcmills): Extract the C++ toolchain version from the .syso file and
+	// record it in the README.
+	updatedLine := fmt.Sprintf(readmeTmpl, syso, *flagRev, goRev)
+
+	lineRE, err := regexp.Compile("^" + fmt.Sprintf(readmeTmpl, regexp.QuoteMeta(syso), commitRE, commitRE) + "$")
+	if err != nil {
+		return err
+	}
+	if lineRE.Match(readme) {
+		readme = lineRE.ReplaceAll(readme, []byte(updatedLine))
+	} else {
+		readme = append(append(readme, []byte(updatedLine)...), '\n')
+	}
+
+	return ioutil.WriteFile(readmeFile, readme, 0640)
+}
+
 func (p *Platform) Gomote(ctx context.Context, args ...string) ([]byte, error) {
 	log.Printf("%v: gomote %v", p.Name(), args)