internal/buildtest: add

Add the buildtest package, to aid running `go build` in tests.
Copied from the x/vuln repo.

Change-Id: I68c38ab28ed893e6cd7334ad79dda1d087ad7f19
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/467717
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/buildtest/buildtest.go b/internal/buildtest/buildtest.go
new file mode 100644
index 0000000..559ca08
--- /dev/null
+++ b/internal/buildtest/buildtest.go
@@ -0,0 +1,97 @@
+// 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.
+
+// Package buildtest provides support for running "go build"
+// in tests.
+package buildtest
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+var unsupportedGoosGoarch = map[string]bool{
+	"darwin/386": true,
+	"darwin/arm": true,
+}
+
+// GoBuild runs "go build" on dir using the additional environment variables in
+// envVarVals, which should be an alternating list of variables and values.
+// It returns the path to the resulting binary, and a function
+// to call when finished with the binary.
+func GoBuild(t *testing.T, dir, tags string, envVarVals ...string) (binaryPath string, cleanup func()) {
+	switch runtime.GOOS {
+	case "android", "js", "ios":
+		t.Skipf("skipping on OS without 'go build' %s", runtime.GOOS)
+	}
+
+	if len(envVarVals)%2 != 0 {
+		t.Fatal("last args should be alternating variables and values")
+	}
+	var env []string
+	if len(envVarVals) > 0 {
+		env = os.Environ()
+		for i := 0; i < len(envVarVals); i += 2 {
+			env = append(env, fmt.Sprintf("%s=%s", envVarVals[i], envVarVals[i+1]))
+		}
+	}
+
+	gg := lookupEnv("GOOS", env, runtime.GOOS) + "/" + lookupEnv("GOARCH", env, runtime.GOARCH)
+	if unsupportedGoosGoarch[gg] {
+		t.Skipf("skipping unsupported GOOS/GOARCH pair %s", gg)
+	}
+
+	tmpDir, err := os.MkdirTemp("", "buildtest")
+	if err != nil {
+		t.Fatal(err)
+	}
+	abs, err := filepath.Abs(dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	binaryPath = filepath.Join(tmpDir, filepath.Base(abs))
+	var exeSuffix string
+	if runtime.GOOS == "windows" {
+		exeSuffix = ".exe"
+	}
+	// Make sure we use the same version of go that is running this test.
+	goCommandPath := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix)
+	if _, err := os.Stat(goCommandPath); err != nil {
+		t.Fatal(err)
+	}
+	args := []string{"build", "-o", binaryPath + exeSuffix}
+	if tags != "" {
+		args = append(args, "-tags", tags)
+	}
+	cmd := exec.Command(goCommandPath, args...)
+	cmd.Dir = dir
+	cmd.Env = env
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		t.Fatal(err)
+	}
+	return binaryPath + exeSuffix, func() { os.RemoveAll(tmpDir) }
+}
+
+// lookEnv looks for name in env, a list of "VAR=VALUE" strings. It returns
+// the value if name is found, and defaultValue if it is not.
+func lookupEnv(name string, env []string, defaultValue string) string {
+	for _, vv := range env {
+		i := strings.IndexByte(vv, '=')
+		if i < 0 {
+			// malformed env entry; just ignore it
+			continue
+		}
+		if name == vv[:i] {
+			return vv[i+1:]
+		}
+	}
+	return defaultValue
+}