Introduce infrastructure for calling and testing nested
commands, error messages and exit codes.

Also:
- implements the -Xclang-path= flag as use case of calling
  a nested command.
- adds tests for forwarding errors, comparing against the
  old wrapper, and exit codes.
- captures the source locations of errors in error messages.
- compares exit codes of new wrapper and old wrapper.

BUG=chromium:773875
TEST=unit test

Change-Id: I919e58091d093d68939809f676f799a68ec7a34e
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/1676833
Reviewed-by: George Burgess <gbiv@chromium.org>
Tested-by: Tobias Bosch <tbosch@google.com>
diff --git a/compiler_wrapper/compiler_wrapper.go b/compiler_wrapper/compiler_wrapper.go
index 6034fb0..4ef3039 100644
--- a/compiler_wrapper/compiler_wrapper.go
+++ b/compiler_wrapper/compiler_wrapper.go
@@ -1,21 +1,84 @@
 package main
 
 import (
-	"log"
+	"fmt"
+	"io"
 	"path/filepath"
 	"strings"
+	"syscall"
 )
 
-func calcCompilerCommand(env env, cfg *config, wrapperCmd *command) (*command, error) {
-	absWrapperDir, err := getAbsWrapperDir(env, wrapperCmd.path)
+func callCompiler(env env, cfg *config, inputCmd *command) int {
+	exitCode := 0
+	var compilerErr error
+	if shouldForwardToOldWrapper(env, inputCmd) {
+		// TODO: Once this is only checking for bisect, create a command
+		// that directly calls the bisect driver in calcCompilerCommand.
+		exitCode, compilerErr = forwardToOldWrapper(env, cfg, inputCmd)
+	} else if cfg.oldWrapperPath != "" {
+		exitCode, compilerErr = callCompilerWithRunAndCompareToOldWrapper(env, cfg, inputCmd)
+	} else {
+		compilerErr = callCompilerWithExec(env, cfg, inputCmd)
+	}
+	if compilerErr != nil {
+		printCompilerError(env.stderr(), compilerErr)
+		exitCode = 1
+	}
+	return exitCode
+}
+
+func callCompilerWithRunAndCompareToOldWrapper(env env, cfg *config, inputCmd *command) (exitCode int, err error) {
+	recordingEnv := &commandRecordingEnv{
+		env: env,
+	}
+	compilerCmd, err := calcCompilerCommand(recordingEnv, cfg, inputCmd)
+	if err != nil {
+		return exitCode, err
+	}
+	exitCode = 0
+	// Note: we are not using env.exec here so that we can compare the exit code
+	// against the old wrapper too.
+	if err := recordingEnv.run(compilerCmd, env.stdout(), env.stderr()); err != nil {
+		if userErr, ok := getCCacheError(compilerCmd, err); ok {
+			return exitCode, userErr
+		}
+		var ok bool
+		if exitCode, ok = getExitCode(err); !ok {
+			return exitCode, wrapErrorwithSourceLocf(err, "failed to execute %s %s", compilerCmd.path, compilerCmd.args)
+		}
+	}
+	if err := compareToOldWrapper(env, cfg, inputCmd, recordingEnv.cmdResults); err != nil {
+		return exitCode, err
+	}
+	return exitCode, nil
+}
+
+func callCompilerWithExec(env env, cfg *config, inputCmd *command) error {
+	compilerCmd, err := calcCompilerCommand(env, cfg, inputCmd)
+	if err != nil {
+		return err
+	}
+	if err := env.exec(compilerCmd); err != nil {
+		// Note: No need to check for exit code error as exec will
+		// stop this control flow once the command started executing.
+		if userErr, ok := getCCacheError(compilerCmd, err); ok {
+			return userErr
+		}
+		return wrapErrorwithSourceLocf(err, "failed to execute %s %s", compilerCmd.path, compilerCmd.args)
+	}
+	return nil
+}
+
+func calcCompilerCommand(env env, cfg *config, inputCmd *command) (*command, error) {
+	if err := checkUnsupportedFlags(inputCmd); err != nil {
+		return nil, err
+	}
+	absWrapperDir, err := getAbsWrapperDir(env, inputCmd.path)
 	if err != nil {
 		return nil, err
 	}
 	rootPath := filepath.Join(absWrapperDir, cfg.rootRelPath)
-	if err := checkUnsupportedFlags(wrapperCmd); err != nil {
-		return nil, err
-	}
-	builder, err := newCommandBuilder(env, cfg, wrapperCmd)
+	builder, err := newCommandBuilder(env, cfg, inputCmd)
 	if err != nil {
 		return nil, err
 	}
@@ -47,56 +110,34 @@
 	return builder.build(), nil
 }
 
-func calcCompilerCommandAndCompareToOld(env env, cfg *config, wrapperCmd *command) (*command, error) {
-	compilerCmd, err := calcCompilerCommand(env, cfg, wrapperCmd)
-	if err != nil {
-		return nil, err
-	}
-	if cfg.oldWrapperPath == "" {
-		return compilerCmd, nil
-	}
-	oldCmds, err := calcOldCompilerCommands(env, cfg, wrapperCmd)
-	if err != nil {
-		return nil, err
-	}
-	if err := compilerCmd.verifySimilarTo(oldCmds[0]); err != nil {
-		return nil, err
-	}
-	return compilerCmd, nil
-}
-
 func getAbsWrapperDir(env env, wrapperPath string) (string, error) {
 	if !filepath.IsAbs(wrapperPath) {
 		wrapperPath = filepath.Join(env.getwd(), wrapperPath)
 	}
 	evaledCmdPath, err := filepath.EvalSymlinks(wrapperPath)
 	if err != nil {
-		log.Printf("Unable to EvalSymlinks for %s. Error: %s", evaledCmdPath, err)
-		return "", err
+		return "", wrapErrorwithSourceLocf(err, "failed to evaluate symlinks for %s", wrapperPath)
 	}
 	return filepath.Dir(evaledCmdPath), nil
 }
 
-// Whether the command should be executed by the old wrapper as we don't
-// support it yet.
-func shouldForwardToOldWrapper(env env, wrapperCmd *command) bool {
-	for _, arg := range wrapperCmd.args {
-		switch {
-		case strings.HasPrefix(arg, "-Xclang-path="):
-			fallthrough
-		case arg == "-clang-syntax":
-			return true
-		}
+func getCCacheError(compilerCmd *command, compilerCmdErr error) (ccacheErr userError, ok bool) {
+	if en, ok := compilerCmdErr.(syscall.Errno); ok && en == syscall.ENOENT &&
+		strings.Contains(compilerCmd.path, "ccache") {
+		ccacheErr =
+			newUserErrorf("ccache not found under %s. Please install it",
+				compilerCmd.path)
+		return ccacheErr, ok
 	}
-	switch {
-	case env.getenv("WITH_TIDY") != "":
-		fallthrough
-	case env.getenv("FORCE_DISABLE_WERROR") != "":
-		fallthrough
-	case env.getenv("GETRUSAGE") != "":
-		fallthrough
-	case env.getenv("BISECT_STAGE") != "":
-		return true
+	return ccacheErr, false
+}
+
+func printCompilerError(writer io.Writer, compilerErr error) {
+	if _, ok := compilerErr.(userError); ok {
+		fmt.Fprintf(writer, "%s\n", compilerErr)
+	} else {
+		fmt.Fprintf(writer,
+			"Internal error. Please report to chromeos-toolchain@google.com.\n%s\n",
+			compilerErr)
 	}
-	return false
 }