all: add a macOS 10.14 (Mojave) builder

Fixes golang/go#27806

Change-Id: I576f563acb2c50cd0456cd4e6a1271b9aa59c9df
Reviewed-on: https://go-review.googlesource.com/c/build/+/169498
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go
index 0f0d718..a58998a 100644
--- a/cmd/buildlet/buildlet.go
+++ b/cmd/buildlet/buildlet.go
@@ -1666,7 +1666,11 @@
 		log.Fatalf("unsupported sw_vers version %q", version)
 	}
 	major, minor := m[1], m[2] // "10", "12"
-	*reverse = "darwin-amd64-" + major + "_" + minor
+	if m, _ := strconv.Atoi(minor); m >= 13 {
+		*reverseType = "host-darwin-10_" + minor
+	} else {
+		*reverse = "darwin-amd64-" + major + "_" + minor
+	}
 	*coordinator = "farmer.golang.org:443"
 
 	// guestName is set by cmd/makemac to something like
diff --git a/cmd/makemac/makemac.go b/cmd/makemac/makemac.go
index 15d6eda..4d52b13 100644
--- a/cmd/makemac/makemac.go
+++ b/cmd/makemac/makemac.go
@@ -47,15 +47,25 @@
 }
 
 var (
-	flagStatus = flag.Bool("status", false, "print status only")
-	flagAuto   = flag.Bool("auto", false, "Automatically create & destroy as needed, reacting to https://farmer.golang.org/status/reverse.json status.")
-	flagListen = flag.String("listen", ":8713", "HTTP status port; used by auto mode only")
-	flagNuke   = flag.Bool("destroy-all", false, "immediately destroy all running Mac VMs")
+	flagStatus   = flag.Bool("status", false, "print status only")
+	flagAuto     = flag.Bool("auto", false, "Automatically create & destroy as needed, reacting to https://farmer.golang.org/status/reverse.json status.")
+	flagListen   = flag.String("listen", ":8713", "HTTP status port; used by auto mode only")
+	flagNuke     = flag.Bool("destroy-all", false, "immediately destroy all running Mac VMs")
+	flagBaseDisk = flag.Int("base-disk", 0, "debug mode: if non-zero, print base disk of macOS 10.<value> VM and exit")
 )
 
 func main() {
 	flag.Parse()
 	numArg := flag.NArg()
+	ctx := context.Background()
+	if *flagBaseDisk != 0 {
+		baseDisk, err := findBaseDisk(ctx, *flagBaseDisk)
+		if err != nil {
+			log.Fatal(err)
+		}
+		fmt.Println(baseDisk)
+		return
+	}
 	if *flagStatus {
 		numArg++
 	}
@@ -72,7 +82,6 @@
 		autoLoop()
 		return
 	}
-	ctx := context.Background()
 	if *flagNuke {
 		state, err := getState(ctx)
 		if err != nil {
@@ -201,24 +210,41 @@
 		guestType = "darwin12_64Guest"
 	case 9:
 		guestType = "darwin13_64Guest"
-	case 10, 11, 12:
+	case 10:
 		guestType = "darwin14_64Guest"
+	case 11:
+		guestType = "darwin15_64Guest"
+	case 12:
+		guestType = "darwin16_64Guest"
+	case 13:
+		// High Sierra. Requires vSphere 6.7.
+		// https://www.virtuallyghetto.com/2018/04/new-vsphere-6-7-apis-worth-checking-out.html
+		guestType = "darwin17_64Guest"
+	case 14:
+		// Mojave. Requires vSphere 6.7.
+		// https://www.virtuallyghetto.com/2018/04/new-vsphere-6-7-apis-worth-checking-out.html
+		guestType = "darwin18_64Guest"
 	default:
 		return "", fmt.Errorf("unsupported makemac minor OS X version %d", minor)
 	}
 
 	builderType := fmt.Sprintf("darwin-amd64-10_%d", minor)
+
+	// Up to 10.12 we used the deprecated buildlet --reverse mode, instead of --reverse-type.
+	// Starting with 10.14 (and 10.13 if we ever make a High Sierra image), we're switching
+	// to the non-deprecated mode.
+	if minor >= 13 {
+		builderType = fmt.Sprintf("host-darwin-10_%d", minor)
+	}
+
 	key, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), "keys", builderType))
 	if err != nil {
 		return "", err
 	}
 
-	// Find the top-level datastore directory hosting the vmdk COW disk for
-	// the linked clone. This is usually named "osx_9_frozen", but may be named
-	// with a "_1", "_2", etc suffix. Search for it.
-	netAppDir, err := findFrozenDir(ctx, minor)
+	baseDisk, err := findBaseDisk(ctx, minor)
 	if err != nil {
-		return "", fmt.Errorf("failed to find osx_%d_frozen base directory: %v", minor, err)
+		return "", fmt.Errorf("failed to find osx_%d_frozen base disk: %v", minor, err)
 	}
 
 	hostNum, hostWhich, err := st.pickHost()
@@ -270,7 +296,7 @@
 		"-link=true",
 		"-persist=false",
 		"-ds=Pure1-1",
-		"-disk", fmt.Sprintf("%s/osx_%d_frozen.vmdk", netAppDir, minor),
+		"-disk", baseDisk,
 	); err != nil {
 		return "", err
 	}
@@ -437,29 +463,62 @@
 		return err
 	}
 	err = json.NewDecoder(stdout).Decode(dst)
-	cmd.Process.Kill() // usually unnecessary
 	if werr := cmd.Wait(); werr != nil && err == nil {
 		err = werr
 	}
 	return err
 }
 
-// findFrozenDir returns the name of the top-level directory on the
-// Pure1-1 shared datastore containing a directory starting with
-// "osx_<minor>_frozen". It might be that just that, or have a suffix
-// like "_1" or "_2".
-func findFrozenDir(ctx context.Context, minor int) (string, error) {
-	out, err := exec.CommandContext(ctx, "govc", "datastore.ls", "-ds=Pure1-1").Output()
+// findBaseDisk returns the path of the vmdk of the most recent
+// snapshot of the osx_$(minor)_frozen VM.
+func findBaseDisk(ctx context.Context, minor int) (string, error) {
+	vmName := fmt.Sprintf("osx_%d_frozen", minor)
+	out, err := exec.CommandContext(ctx, "govc", "vm.info", "-json", vmName).Output()
 	if err != nil {
 		return "", err
 	}
-	prefix := fmt.Sprintf("osx_%d_frozen", minor)
-	for _, dir := range strings.Fields(string(out)) {
-		if strings.HasPrefix(dir, prefix) {
-			return dir, nil
+	var ret struct {
+		VirtualMachines []struct {
+			Layout struct {
+				Snapshot []struct {
+					SnapshotFile []string
+				}
+			}
 		}
 	}
-	return "", os.ErrNotExist
+	if err := json.Unmarshal(out, &ret); err != nil {
+		return "", fmt.Errorf("failed to parse vm.info JSON to find base disk: %v", err)
+	}
+	if n := len(ret.VirtualMachines); n != 1 {
+		if n == 0 {
+			return "", fmt.Errorf("VM %s not found", vmName)
+		}
+		return "", fmt.Errorf("len(ret.VirtualMachines) = %d; want 1 in JSON to find base disk: %v", n, err)
+	}
+	vm := ret.VirtualMachines[0]
+	if len(vm.Layout.Snapshot) < 1 {
+		return "", fmt.Errorf("VM %s does not have any snapshots. Needs at least one.", vmName)
+	}
+	ss := vm.Layout.Snapshot[len(vm.Layout.Snapshot)-1] // most recent snapshot is last in list
+
+	// Now find the first vmdk file, without its [datastore] prefix. The files are listed like:
+	/*
+	   "SnapshotFile": [
+	     "[Pure1-1] osx_14_frozen/osx_14_frozen-Snapshot2.vmsn",
+	     "[Pure1-1] osx_14_frozen/osx_14_frozen_15.vmdk",
+	     "[Pure1-1] osx_14_frozen/osx_14_frozen_15-000001.vmdk"
+	   ]
+	*/
+	for _, f := range ss.SnapshotFile {
+		if strings.HasSuffix(f, ".vmdk") {
+			i := strings.Index(f, "] ")
+			if i == -1 {
+				return "", fmt.Errorf("unexpected vmdk line %q in SnapshotFile", f)
+			}
+			return f[i+2:], nil
+		}
+	}
+	return "", fmt.Errorf("no VMDK found in snapshot for %v", vmName)
 }
 
 const autoAdjustTimeout = 5 * time.Minute
@@ -472,33 +531,10 @@
 }
 
 func init() {
-	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		status.Lock()
-		defer status.Unlock()
-		w.Header().Set("Content-Type", "application/json")
-
-		// Locking the lastState shouldn't matter since we
-		// currently only set status.lastState once the
-		// *Status is no longer in use, but lock it anyway, in
-		// case usage changes in the future.
-		if st := status.lastState; st != nil {
-			st.mu.Lock()
-			defer st.mu.Unlock()
-		}
-
-		// TODO: probably more status, as needed.
-		res := &struct {
-			LastCheck string
-			LastLog   string
-			LastState *State
-		}{
-			LastCheck: status.lastCheck.UTC().Format(time.RFC3339),
-			LastLog:   status.lastLog,
-			LastState: status.lastState,
-		}
-		j, _ := json.MarshalIndent(res, "", "\t")
-		w.Write(j)
-	})
+	http.HandleFunc("/stage0/", handleStage0)
+	http.HandleFunc("/buildlet.darwin-amd64", handleBuildlet)
+	http.Handle("/", onlyAtRoot{http.HandlerFunc(handleStatus)}) // legacy status location
+	http.HandleFunc("/status", handleStatus)
 }
 
 func dedupLogf(format string, args ...interface{}) {
@@ -659,3 +695,131 @@
 	}
 	return 0
 }
+
+func handleStatus(w http.ResponseWriter, r *http.Request) {
+	status.Lock()
+	defer status.Unlock()
+	w.Header().Set("Content-Type", "application/json")
+
+	// Locking the lastState shouldn't matter since we
+	// currently only set status.lastState once the
+	// *Status is no longer in use, but lock it anyway, in
+	// case usage changes in the future.
+	if st := status.lastState; st != nil {
+		st.mu.Lock()
+		defer st.mu.Unlock()
+	}
+
+	// TODO: probably more status, as needed.
+	res := &struct {
+		LastCheck string
+		LastLog   string
+		LastState *State
+	}{
+		LastCheck: status.lastCheck.UTC().Format(time.RFC3339),
+		LastLog:   status.lastLog,
+		LastState: status.lastState,
+	}
+	j, _ := json.MarshalIndent(res, "", "\t")
+	w.Write(j)
+}
+
+// handleStage0 serves the shell script for buildlets to run on boot, based
+// on their macOS version.
+//
+// Starting with the macOS 10.14 (Mojave) image, their baked-in stage0.sh
+// script does:
+//
+//    while true; do (curl http://10.50.0.2:8713/stage0/$(sw_vers -productVersion)| sh); sleep 5; done
+func handleStage0(w http.ResponseWriter, r *http.Request) {
+	// ver will be like "10.14.4"
+	// Nothing currently uses this, but it might be useful in the future.
+	ver := strings.TrimPrefix(r.RequestURI, "/stage0/")
+	_ = ver
+
+	fmt.Fprintf(w, "set -e\nset -x\n")
+	fmt.Fprintf(w, "export GO_BUILDER_ENV=macstadium_vm\n")
+	fmt.Fprintf(w, "curl -o buildlet http://10.50.0.2:8713/buildlet.darwin-amd64\n")
+	fmt.Fprintf(w, "chmod +x buildlet; ./buildlet")
+}
+
+func handleBuildlet(w http.ResponseWriter, r *http.Request) {
+	bin, err := getLatestMacBuildlet(r.Context())
+	if err != nil {
+		log.Printf("error getting buildlet from GCS: %v", err)
+		http.Error(w, "error getting buildlet from GCS", 500)
+	}
+	w.Header().Set("Content-Length", fmt.Sprint(len(bin)))
+	w.Write(bin)
+}
+
+// buildlet binary caching by its last seen ETag from HEAD responses
+var (
+	buildletMu   sync.Mutex
+	lastEtag     string
+	lastBuildlet []byte // last buildlet binary for lastEtag
+)
+
+func getLatestMacBuildlet(ctx context.Context) (bin []byte, err error) {
+	req, _ := http.NewRequest("HEAD", "https://storage.googleapis.com/go-builder-data/buildlet.darwin-amd64", nil)
+	req = req.WithContext(ctx)
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	if res.StatusCode != 200 {
+		return nil, fmt.Errorf("%s from HEAD to %s", res.Status, req.URL)
+	}
+	etag := res.Header.Get("Etag")
+	if etag == "" {
+		return nil, fmt.Errorf("HEAD of %s lacked ETag", req.URL)
+	}
+
+	buildletMu.Lock()
+	if etag == lastEtag {
+		bin = lastBuildlet
+		log.Printf("served cached buildlet of %s", etag)
+		buildletMu.Unlock()
+		return bin, nil
+	}
+	buildletMu.Unlock()
+
+	log.Printf("fetching buildlet from GCS...")
+	req, _ = http.NewRequest("GET", "https://storage.googleapis.com/go-builder-data/buildlet.darwin-amd64", nil)
+	req = req.WithContext(ctx)
+	res, err = http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	if res.StatusCode != 200 {
+		return nil, fmt.Errorf("%s from GET to %s", res.Status, req.URL)
+	}
+	etag = res.Header.Get("Etag")
+	log.Printf("fetched buildlet from GCS with etag %s", etag)
+	if etag == "" {
+		return nil, fmt.Errorf("GET of %s lacked ETag", req.URL)
+	}
+	slurp, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	buildletMu.Lock()
+	defer buildletMu.Unlock()
+	lastEtag = etag
+	lastBuildlet = slurp
+	return lastBuildlet, nil
+}
+
+// onlyAtRoot is an http.Handler wrapper that enforces that it's
+// called at /, else it serves a 404.
+type onlyAtRoot struct{ h http.Handler }
+
+func (h onlyAtRoot) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if r.URL.Path != "/" {
+		http.NotFound(w, r)
+		return
+	}
+	h.h.ServeHTTP(w, r)
+}
diff --git a/dashboard/builders.go b/dashboard/builders.go
index 237b515..96cb88b 100644
--- a/dashboard/builders.go
+++ b/dashboard/builders.go
@@ -353,7 +353,7 @@
 	},
 	"host-darwin-10_10": &HostConfig{
 		IsReverse: true,
-		ExpectNum: 1,
+		ExpectNum: 3,
 		Notes:     "MacStadium OS X 10.10 VM under VMWare ESXi",
 		env: []string{
 			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
@@ -364,7 +364,7 @@
 	},
 	"host-darwin-10_11": &HostConfig{
 		IsReverse: true,
-		ExpectNum: 17,
+		ExpectNum: 7,
 		Notes:     "MacStadium OS X 10.11 VM under VMWare ESXi",
 		env: []string{
 			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
@@ -375,7 +375,7 @@
 	},
 	"host-darwin-10_12": &HostConfig{
 		IsReverse: true,
-		ExpectNum: 2,
+		ExpectNum: 3,
 		Notes:     "MacStadium OS X 10.12 VM under VMWare ESXi",
 		env: []string{
 			"GOROOT_BOOTSTRAP=/Users/gopher/go1.4",
@@ -384,6 +384,16 @@
 		SSHUsername:     "gopher",
 		HermeticReverse: true, // we destroy the VM when done & let cmd/makemac recreate
 	},
+	"host-darwin-10_14": &HostConfig{
+		IsReverse: true,
+		ExpectNum: 7,
+		Notes:     "MacStadium macOS Mojave (10.14) VM under VMWare ESXi",
+		env: []string{
+			"GOROOT_BOOTSTRAP=/Users/gopher/goboot", // Go 1.12.1
+		},
+		SSHUsername:     "gopher",
+		HermeticReverse: true, // we destroy the VM when done & let cmd/makemac recreate
+	},
 	"host-linux-s390x": &HostConfig{
 		Notes:          "run by IBM",
 		OwnerGithub:    "mundaym",
@@ -1202,10 +1212,10 @@
 
 func init() {
 	addBuilder(BuildConfig{
-		Name:      "freebsd-amd64-gce93",
-		HostType:  "host-freebsd-93-gce",
-		tryOnly:   true, // don't run regular build...
-		MaxAtOnce: 2,
+		Name:       "freebsd-amd64-gce93",
+		HostType:   "host-freebsd-93-gce",
+		buildsRepo: disabledBuilder,
+		MaxAtOnce:  2,
 	})
 	addBuilder(BuildConfig{
 		Name:     "freebsd-amd64-10_3",
@@ -1340,11 +1350,10 @@
 		RunBench:          true,
 	})
 	addBuilder(BuildConfig{
-		Name:      "linux-amd64-vmx",
-		HostType:  "host-linux-stretch-vmx",
-		MaxAtOnce: 1,
-		tryOnly:   true, // don't run regular build
-		tryBot:    nil,  // and don't run trybots (only gomote)
+		Name:       "linux-amd64-vmx",
+		HostType:   "host-linux-stretch-vmx",
+		MaxAtOnce:  1,
+		buildsRepo: disabledBuilder,
 	})
 
 	const testAlpine = false // Issue 22689 (hide all red builders), Issue 19938 (get Alpine passing)
@@ -1653,8 +1662,7 @@
 		Name:              "openbsd-amd64-60",
 		HostType:          "host-openbsd-amd64-60",
 		shouldRunDistTest: noTestDir,
-		tryOnly:           true, // disabled by default; Go 1.11+ don't support it anymore
-		tryBot:            nil,
+		buildsRepo:        disabledBuilder,
 		MaxAtOnce:         1,
 		numTestHelpers:    2,
 		numTryTestHelpers: 5,
@@ -1663,8 +1671,7 @@
 		Name:              "openbsd-386-60",
 		HostType:          "host-openbsd-386-60",
 		shouldRunDistTest: noTestDir,
-		tryOnly:           true, // disabled by default; Go 1.11+ don't support it anymore
-		tryBot:            nil,
+		buildsRepo:        disabledBuilder,
 		MaxAtOnce:         1,
 		env: []string{
 			// cmd/go takes ~192 seconds on openbsd-386
@@ -1737,8 +1744,7 @@
 		MaxAtOnce:         1,
 		// This builder currently hangs in the “../test” phase of all.bash.
 		// (https://golang.org/issue/25206)
-		tryOnly: true, // Disable regular builds.
-		tryBot:  nil,  // Disable trybots.
+		buildsRepo: disabledBuilder,
 	})
 	addBuilder(BuildConfig{
 		Name:              "netbsd-arm-bsiegert",
@@ -1847,14 +1853,18 @@
 		Name:              "darwin-amd64-10_8",
 		HostType:          "host-darwin-10_8",
 		shouldRunDistTest: noTestDir,
-		tryOnly:           true, // but not in trybot set, so effectively disabled
-		tryBot:            nil,
+		buildsRepo:        disabledBuilder,
 	})
 	addBuilder(BuildConfig{
 		Name:              "darwin-amd64-10_10",
 		HostType:          "host-darwin-10_10",
 		shouldRunDistTest: noTestDir,
-		buildsRepo:        onlyGo,
+		buildsRepo: func(repo, branch, goBranch string) bool {
+			// https://tip.golang.org/doc/go1.12 says:
+			// "Go 1.12 is the last release that will run on macOS 10.10 Yosemite."
+			major, minor, ok := version.ParseReleaseBranch(branch)
+			return repo == "go" && ok && major == 1 && minor <= 12
+		},
 	})
 	addBuilder(BuildConfig{
 		Name:              "darwin-amd64-10_11",
@@ -1878,6 +1888,11 @@
 		shouldRunDistTest: noTestDir,
 	})
 	addBuilder(BuildConfig{
+		Name:              "darwin-amd64-10_14",
+		HostType:          "host-darwin-10_14",
+		shouldRunDistTest: noTestDir,
+	})
+	addBuilder(BuildConfig{
 		Name:              "darwin-amd64-race",
 		HostType:          "host-darwin-10_12",
 		shouldRunDistTest: noTestDir,
@@ -2212,3 +2227,6 @@
 
 // onlyGo is a common buildsRepo policy value that only builds the main "go" repo.
 func onlyGo(repo, branch, goBranch string) bool { return repo == "go" }
+
+// disabledBuilder is a buildsRepo policy function that always return false.
+func disabledBuilder(repo, branch, goBranch string) bool { return false }
diff --git a/dashboard/builders_test.go b/dashboard/builders_test.go
index 03eff19..cbae025 100644
--- a/dashboard/builders_test.go
+++ b/dashboard/builders_test.go
@@ -426,6 +426,16 @@
 		{b("darwin-amd64-10_11@go1.11", "net"), none},
 		{b("darwin-amd64-10_11@go1.12", "net"), none},
 		{b("darwin-386-10_11@go1.11", "net"), none},
+
+		{b("darwin-amd64-10_14", "go"), onlyPost},
+		{b("darwin-amd64-10_12", "go"), onlyPost},
+		{b("darwin-amd64-10_11", "go"), onlyPost},
+		{b("darwin-amd64-10_10", "go"), none},
+		{b("darwin-amd64-10_10@go1.12", "go"), onlyPost},
+		{b("darwin-amd64-10_10@go1.11", "go"), onlyPost},
+		{b("darwin-386-10_11", "go"), onlyPost},
+		{b("darwin-386-10_11@go1.12", "go"), onlyPost},
+		{b("darwin-386-10_11@go1.11", "go"), onlyPost},
 	}
 	for _, tt := range tests {
 		t.Run(tt.br.testName, func(t *testing.T) {
diff --git a/env/darwin/macstadium/image-setup-notes.txt b/env/darwin/macstadium/image-setup-notes.txt
index 2356a39..0db6946 100644
--- a/env/darwin/macstadium/image-setup-notes.txt
+++ b/env/darwin/macstadium/image-setup-notes.txt
@@ -1,4 +1,16 @@
-$HOME/go1.4
+Install VMWare tools daemon.
+
+  - you should be able to do this from the vSphere UI, but I got errors with Mojave.
+  - backup plan: https://my.vmware.com/web/vmware/details?productId=742&downloadGroup=VMTOOLS1032
+    and then copy the darwin.iso to the host and install it manually.
+  - open security preferences and click "Allow" on blocked software install from VMware
+  - reboot
+  - make sure you can run and see:
+
+    $ /Library/Application Support/VMware Tools/vmware-tools-daemon --cmd "info-get guestinfo.name"
+    No value value
+
+Add $HOME/go1.4
 
 System Preferences > Software Update > off
 
@@ -8,27 +20,35 @@
 
 System Preferences > Sharing > enable ssh (for later)
 
-curl -o stage0.sh https://....
+Create executable $HOME/stage0.sh with:
 
-chmod +x stage0.sh
+   #!/bin/bash
+   while true; do (curl -v http://10.50.0.2:8713/stage0/$(sw_ver -productVersion) | sh); sleep 5; done
 
-Automator > Create application > Run shell Script > "open -b com.apple.terminal $HOME/stage0.sh", save to desktop
+Automator:
+
+    File > New > Application
+    [+] Run shell script
+    [ open -a Terminal.app $HOME/stage0.sh ]
+    Save to desktop as "run-builder"
 
 System Preferences > Users & Groups > auto-login "gopher" user, run Desktop/run-builder (automator app)
 
-passwordless sudo
+passwordless sudo:
+
+   sudo visudo
+   Change line from:
+    %admin ALL=(ALL) ALL
+   to:
+    %admin ALL=(ALL) NOPASSWD: ALL
 
 install xcode
   (as of 10.10 or 10.9, running git first time will propt for install;
    before that, need to find old xcode version)
 
-sudo visudo
-Change line from:
-  %admin ALL=(ALL) ALL
-to:
-  %admin ALL=(ALL) NOPASSWD: ALL
+verbose boot: (text instead of apple image)
 
-verbose boot:
+    sudo nvram boot-args="-v"
 
 run-builder-darwin-10_11.sh