cmd/runqemubuildlet: add darwin support

This is used to replace env/darwin/aws/start-snapshot.sh on CI
instances.

Run like so:

Buildlet 1:
$ ./runqemubuildlet -guest-os=darwin -macos-version=12 -osk=<OSK VAL> -guest-index=1 -buildlet-healthz-url="http://192.168.64.101:8080/healthz"

Buildlet 2:
$ ./runqemubuildlet -guest-os=darwin -macos-version=12 -osk=<OSK VAL> -guest-index=2 -buildlet-healthz-url="http://192.168.64.102:8080/healthz"

For golang/go#48945.

Change-Id: Ic0010bcd0062c7b77d09bf2addb1297e7d116474
Reviewed-on: https://go-review.googlesource.com/c/build/+/449877
Reviewed-by: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Jenny Rakoczy <jenny@golang.org>
Run-TryBot: Michael Pratt <mpratt@google.com>
diff --git a/cmd/runqemubuildlet/darwin.go b/cmd/runqemubuildlet/darwin.go
new file mode 100644
index 0000000..577d4c8
--- /dev/null
+++ b/cmd/runqemubuildlet/darwin.go
@@ -0,0 +1,91 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.16
+// +build go1.16
+
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+// defaultDarwinDir returns a default path for a darwin VM.
+//
+// The directory should contain the darwin VM image, and QEMU
+// (sysroot-macos-x86_64).
+func defaultDarwinDir() string {
+	home, err := os.UserHomeDir()
+	if err != nil {
+		log.Printf("os.UserHomeDir() = %q, %v", home, err)
+		return ""
+	}
+	return home
+}
+
+// darwinCmd returns a qemu command for running a darwin VM, ready
+// to be started.
+func darwinCmd(base string) *exec.Cmd {
+	if *macosVersion == 0 {
+		log.Fatalf("-macos-version required")
+	}
+	if *osk == "" {
+		log.Fatalf("-osk required")
+	}
+
+	sysroot := filepath.Join(base, "sysroot-macos-x86_64")
+	ovmfCode := filepath.Join(sysroot, "share/qemu/edk2-x86_64-code.fd")
+
+	disk := filepath.Join(base, "macos.qcow2")
+
+	// vmnet-shared requires that we run QEMU as root.
+	//
+	// These arguments should be kept in sync with env/darwin/aws/qemu.sh.
+	args := []string{
+		"env",
+		fmt.Sprintf("DYLD_LIBRARY_PATH=%s", filepath.Join(sysroot, "lib")),
+		filepath.Join(sysroot, "bin/qemu-system-x86_64"),
+		// Discard disk changes on exit.
+		"-snapshot",
+		"-m", "4096",
+		"-cpu", "host",
+		"-machine", "q35",
+		"-usb",
+		"-device", "usb-kbd",
+		"-device", "usb-tablet",
+		// macOS only likes a power-of-two number of cores, but odd socket count is
+		// fine.
+		"-smp", "cpus=6,sockets=3,cores=2,threads=1",
+		"-device", "usb-ehci,id=ehci",
+		"-device", "nec-usb-xhci,id=xhci",
+		"-global", "nec-usb-xhci.msi=off",
+		"-device", fmt.Sprintf("isa-applesmc,osk=%s", *osk),
+		"-drive", fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", ovmfCode),
+		"-smbios", "type=2",
+		"-device", "ich9-intel-hda",
+		"-device", "hda-duplex",
+		"-device", "ich9-ahci,id=sata",
+		"-drive", fmt.Sprintf("id=MacHDD,if=none,format=qcow2,file=%s", disk),
+		"-device", "ide-hd,bus=sata.2,drive=MacHDD",
+		"-monitor", "stdio",
+		"-device", "VGA,vgamem_mb=128",
+		"-M", "accel=hvf",
+		"-display", fmt.Sprintf("vnc=127.0.0.1:%d", *guestIndex),
+		// DHCP range is a dummy range. The actual guest IP is assigned statically
+		// based on the MAC address matching an entry in /etc/bootptab.
+		"-netdev", "vmnet-shared,id=net0,start-address=192.168.64.1,end-address=192.168.64.100,subnet-mask=255.255.255.0",
+	}
+	if *macosVersion >= 11 {
+		args = append(args, "-device", fmt.Sprintf("virtio-net-pci,netdev=net0,id=net0,mac=52:54:00:c9:18:0%d", *guestIndex))
+	} else {
+		args = append(args, "-device", fmt.Sprintf("vmxnet3,netdev=net0,id=net0,mac=52:54:00:c9:18:0%d", *guestIndex))
+	}
+
+	cmd := exec.Command("sudo", args...)
+	return cmd
+}
diff --git a/cmd/runqemubuildlet/main.go b/cmd/runqemubuildlet/main.go
index e7baeff..e001c47 100644
--- a/cmd/runqemubuildlet/main.go
+++ b/cmd/runqemubuildlet/main.go
@@ -23,11 +23,19 @@
 
 var (
 	// Common flags
-	guestOS    = flag.String("guest-os", "windows", "Guest OS to run (one of: windows)")
+	guestOS    = flag.String("guest-os", "windows", "Guest OS to run (one of: windows or darwin)")
 	healthzURL = flag.String("buildlet-healthz-url", "http://localhost:8080/healthz", "URL to buildlet /healthz endpoint.")
 
 	// -guest-os=windows flags
 	windows10Path = flag.String("windows-10-path", defaultWindowsDir(), "Path to Windows image and QEMU dependencies.")
+
+	// -guest-os=darwin flags
+	darwinPath = flag.String("darwin-path", defaultDarwinDir(), "Path to darwin image and QEMU dependencies.")
+	// Using an int for this isn't great, but the only thing we need to do
+	// is check if the version is >= 11.
+	macosVersion = flag.Int("macos-version", 0, "macOS major version of guest image (e.g., 10, 11, 12, or 13)")
+	guestIndex   = flag.Int("guest-index", 1, "Index indicating which of the two instances on this host that this is (one of: 1 or 2)")
+	osk          = flag.String("osk", "", "Apple OSK key value")
 )
 
 func main() {
@@ -39,6 +47,8 @@
 	for ctx.Err() == nil {
 		var cmd *exec.Cmd
 		switch *guestOS {
+		case "darwin":
+			cmd = darwinCmd(*darwinPath)
 		case "windows":
 			cmd = windows10Cmd(*windows10Path)
 		default:
diff --git a/env/darwin/aws/README.md b/env/darwin/aws/README.md
index 0ad5d84..5e7ed05 100644
--- a/env/darwin/aws/README.md
+++ b/env/darwin/aws/README.md
@@ -144,8 +144,8 @@
    1. Available as `Sysroot-macos-x86_64` in
       https://github.com/utmapp/UTM/actions?query=event%3Arelease builds.
 3. Copy `bootptab` to `/etc/bootptab`.
-4. Restart the system DHCP server to pick up the new `bootptab`: `sudo /bin/launchctl unload -w /System/Library/LaunchDaemons/bootps.plist && sudo /bin/launchctl load -w /System/Library/LaunchDaemons/bootps.plist`.
-4. Copy `qemu.sh` and `start-snapshot.sh` to `$HOME`.
+4. Restart the system DHCP server to pick up the new `bootptab`: `sudo /bin/launchctl unload -w /System/Library/LaunchDaemons/bootps.plist; sudo /bin/launchctl load -w /System/Library/LaunchDaemons/bootps.plist`.
+4. Build `golang.org/x/build/cmd/runqemubuildlet` and copy it to `$HOME`.
 5. Create `$HOME/loop1.sh`:
 
 ```sh
@@ -153,7 +153,7 @@
 
 while true; do
   echo "Running QEMU..."
-  $HOME/start-snapshot.sh $HOME/macos.qcow2 ${OSK_VALUE?} 1
+  $HOME/runqemubuildlet -guest-os=darwin -macos-version=${MACOS_VERSION?} -osk=${OSK_VALUE?} -guest-index=1 -buildlet-healthz-url="http://192.168.64.101:8080/healthz"
 done
 ```
 
@@ -164,7 +164,7 @@
 
 while true; do
   echo "Running QEMU..."
-  $HOME/start-snapshot.sh $HOME/macos.qcow2 ${OSK_VALUE?} 2
+  $HOME/runqemubuildlet -guest-os=darwin -macos-version=${MACOS_VERSION?} -osk=${OSK_VALUE?} -guest-index=2 -buildlet-healthz-url="http://192.168.64.102:8080/healthz"
 done
 ```
 
diff --git a/env/darwin/aws/qemu.sh b/env/darwin/aws/qemu.sh
index 499fba9..fb1f35a 100755
--- a/env/darwin/aws/qemu.sh
+++ b/env/darwin/aws/qemu.sh
@@ -17,6 +17,7 @@
 
 OVMF_CODE="$HOME/sysroot-macos-x86_64/share/qemu/edk2-x86_64-code.fd"
 
+# These arguments should be kept in sync with cmd/runqemubuildlet/darwin.go.
 args=(
   -m 4096
   -cpu host