Kokoro: Check that build files don't reference non-existent files

This is a primitive check to ensure that .bp, .gn and .bazel files don't have source references to missing files.

This can catch a common build breakage.

Fixes: b/153439736
Change-Id: I95621f8775a0e536015d4da9897785935f130884
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/43572
Kokoro-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Nicolas Capens <nicolascapens@google.com>
Tested-by: Ben Clayton <bclayton@google.com>
diff --git a/tests/check_build_files/main.go b/tests/check_build_files/main.go
new file mode 100644
index 0000000..8ab972d
--- /dev/null
+++ b/tests/check_build_files/main.go
@@ -0,0 +1,190 @@
+// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// check_build_files scans all the .bp, .gn and .bazel files for source
+// references to non-existent files.
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+)
+
+func cwd() string {
+	wd, err := os.Getwd()
+	if err != nil {
+		return ""
+	}
+	return wd
+}
+
+var root = flag.String("root", cwd(), "root project directory")
+
+func main() {
+	flag.Parse()
+
+	if err := run(); err != nil {
+		fmt.Fprintf(os.Stderr, "%v", err)
+		os.Exit(1)
+	}
+	fmt.Printf("Build file check completed with no errors\n")
+}
+
+func run() error {
+	wd := *root
+
+	errs := []error{}
+
+	filepath.Walk(wd, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		rel, err := filepath.Rel(wd, path)
+		if err != nil {
+			return filepath.SkipDir
+		}
+
+		switch rel {
+		case ".git", "cache", "build", "out":
+			return filepath.SkipDir
+		}
+
+		if info.IsDir() {
+			return nil
+		}
+
+		content, err := ioutil.ReadFile(path)
+		if err != nil {
+			errs = append(errs, err)
+			return nil // Continue walking files
+		}
+
+		switch filepath.Ext(path) {
+		case ".bp":
+			errs = append(errs, checkBlueprint(path, string(content))...)
+		case ".gn":
+			errs = append(errs, checkGn(path, string(content))...)
+		case ".bazel":
+			errs = append(errs, checkBazel(path, string(content))...)
+		}
+
+		return nil
+	})
+
+	sb := strings.Builder{}
+	for _, err := range errs {
+		sb.WriteString(err.Error())
+		sb.WriteString("\n")
+	}
+	if sb.Len() > 0 {
+		return fmt.Errorf("%v", sb.String())
+	}
+	return nil
+}
+
+var (
+	reSources = regexp.MustCompile(`sources\s*=\s*\[([^\]]*)\]`)
+	reSrc     = regexp.MustCompile(`srcs\s*[:=]\s*\[([^\]]*)\]`)
+	reQuoted  = regexp.MustCompile(`"([^\"]*)"`)
+)
+
+func checkBlueprint(path, content string) []error {
+	errs := []error{}
+	for _, sources := range matchRE(reSrc, content) {
+		for _, source := range matchRE(reQuoted, sources) {
+			if strings.HasPrefix(source, ":") {
+				continue // Build target, we can't resolve.
+			}
+			if err := checkSource(path, source); err != nil {
+				errs = append(errs, err)
+			}
+		}
+	}
+	return errs
+}
+
+func checkGn(path, content string) []error {
+	errs := []error{}
+	for _, sources := range matchRE(reSources, content) {
+		for _, source := range matchRE(reQuoted, sources) {
+			if strings.ContainsAny(source, "$") {
+				return nil // Env vars we can't resolve
+			}
+			if strings.HasPrefix(source, "//") {
+				continue // Build target, we can't resolve.
+			}
+			if err := checkSource(path, source); err != nil {
+				errs = append(errs, err)
+			}
+		}
+	}
+	return errs
+}
+
+func checkBazel(path, content string) []error {
+	errs := []error{}
+	for _, sources := range matchRE(reSrc, content) {
+		for _, source := range matchRE(reQuoted, sources) {
+			if strings.HasPrefix(source, "@") || strings.HasPrefix(source, ":") {
+				continue // Build target, we can't resolve.
+			}
+			if err := checkSource(path, source); err != nil {
+				errs = append(errs, err)
+			}
+		}
+	}
+	return errs
+}
+
+func checkSource(path, source string) error {
+	source = filepath.Join(filepath.Dir(path), source)
+
+	if strings.Contains(source, "*") {
+		sources, err := filepath.Glob(source)
+		if err != nil {
+			return fmt.Errorf("In '%v': %w", path, err)
+		}
+		if len(sources) == 0 {
+			return fmt.Errorf("In '%v': Glob '%v' does not reference any files", path, source)
+		}
+		return nil
+	}
+
+	stat, err := os.Stat(source)
+	if err != nil {
+		return fmt.Errorf("In '%v': %w", path, err)
+	}
+	if stat.IsDir() {
+		return fmt.Errorf("In '%v': '%v' refers to a directory, not a file", path, source)
+	}
+	return nil
+}
+
+func matchRE(re *regexp.Regexp, text string) []string {
+	out := []string{}
+	for _, match := range re.FindAllStringSubmatch(text, -1) {
+		if len(match) < 2 {
+			return nil
+		}
+		out = append(out, match[1])
+	}
+	return out
+}
diff --git a/tests/presubmit.sh b/tests/presubmit.sh
index 122c51c..810309b 100755
--- a/tests/presubmit.sh
+++ b/tests/presubmit.sh
@@ -80,6 +80,10 @@
   find ${SRC_DIR} ${TESTS_DIR} -name "*.go" | xargs $GOFMT -w
 }
 
+function run_check_build_files() {
+  go run ${TESTS_DIR}/check_build_files/main.go --root="${ROOT_DIR}"
+}
+
 # Ensure we are clean to start out with.
 check "git workspace must be clean" true
 
@@ -95,5 +99,8 @@
 # Check gofmt.
 check gofmt run_gofmt
 
+# Check build files.
+check "build files don't reference non-existent files" run_check_build_files
+
 echo
 echo "${green}All check completed successfully.${normal}"