internal/buildbinary: add buildbinary pkg

The buildbinary package finds and builds all binaries in a given module.
This will be used to do binary vs source mode comparisons for govulncheck scan.

Change-Id: I6a3db78cc3b632df09d97771edd8ad9419e8206f
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/506518
Run-TryBot: Maceo Thompson <maceothompson@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/buildbinary/bin.go b/internal/buildbinary/bin.go
new file mode 100644
index 0000000..3db7682
--- /dev/null
+++ b/internal/buildbinary/bin.go
@@ -0,0 +1,60 @@
+// 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 buildbinary
+
+import (
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/pkgsite-metrics/internal/derrors"
+)
+
+// TODO: Consider making struct if we want to pass successful binaries & still
+// be aware if building others failed for some reason.
+
+// FindAndBuildBinaries finds and builds all possible binaries from a given module.
+func FindAndBuildBinaries(modulePath string) (binaries []string, err error) {
+	defer derrors.Wrap(&err, "FindAndBuildBinaries")
+	buildTargets, err := findBinaries(modulePath)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, target := range buildTargets {
+		path, err := runBuild(modulePath, target)
+		if err != nil {
+			return nil, err
+		}
+		binaries = append(binaries, path)
+	}
+	return binaries, nil
+}
+
+// runBuild takes a given module and import path and attempts to build a binary
+func runBuild(modulePath, importPath string) (binaryPath string, err error) {
+	cmd := exec.Command("go", "build", "-C", modulePath, importPath)
+	if err = cmd.Run(); err != nil {
+		return "", err
+	}
+	binaryPath = filepath.Join(modulePath, filepath.Base(importPath))
+	return binaryPath, nil
+}
+
+// findBinaries finds all packages that compile to binaries in a given directory
+// and returns a list of those package's import paths.
+func findBinaries(dir string) (buildTargets []string, err error) {
+	// Running go list with the given arguments only prints the import paths of
+	// packages with package "main", that is packages that could potentially
+	// be built into binaries.
+	cmd := exec.Command("go", "list", "-f", `{{ if eq .Name "main" }} {{ .ImportPath }} {{end}}`, "./...")
+	cmd.Dir = dir
+	out, err := cmd.Output()
+	if err != nil {
+		return nil, err
+	}
+
+	return strings.Fields(string(out)), nil
+}
diff --git a/internal/buildbinary/bin_test.go b/internal/buildbinary/bin_test.go
new file mode 100644
index 0000000..f1561c4
--- /dev/null
+++ b/internal/buildbinary/bin_test.go
@@ -0,0 +1,45 @@
+// 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 buildbinary
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func less(a, b string) bool {
+	return a < b
+}
+
+func TestFindBinaries(t *testing.T) {
+	tests := []struct {
+		name    string
+		dir     string
+		want    []string
+		wantErr bool
+	}{
+		{
+			name:    "local test",
+			dir:     "../testdata/module",
+			want:    []string{"golang.org/vuln"},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := findBinaries(tt.dir)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("findBinaries() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if diff := cmp.Diff(tt.want, got, cmpopts.SortSlices(less)); diff != "" {
+				t.Errorf("mismatch (-want, +got):%s", diff)
+			}
+		})
+	}
+}