sbox: best-effort copy output files on failure

Error messages printed by failing commands may reference output files
that were created by the command, for example printing a command line
to copy and paste to update a baseline file.  Copy output files in the
sandbox to their final locations, ignoring missing files, so that the
messages are valid.

Bug: 185516277
Test: m out/soong/.intermediates/frameworks/base/system-api-stubs-docs-non-updatable/android_common/metalava/api_lint.timestamp with lint error
Change-Id: I604a11c9b54e409ca5bc5c016cd04b3133f74a60
diff --git a/cmd/sbox/sbox.go b/cmd/sbox/sbox.go
index 7bd0868..9736ff6 100644
--- a/cmd/sbox/sbox.go
+++ b/cmd/sbox/sbox.go
@@ -230,7 +230,7 @@
 	}
 
 	// Copy in any files specified by the manifest.
-	err = copyFiles(command.CopyBefore, "", tempDir)
+	err = copyFiles(command.CopyBefore, "", tempDir, false)
 	if err != nil {
 		return "", err
 	}
@@ -276,6 +276,14 @@
 	}
 	err = cmd.Run()
 
+	if err != nil {
+		// The command failed, do a best effort copy of output files out of the sandbox.  This is
+		// especially useful for linters with baselines that print an error message on failure
+		// with a command to copy the output lint errors to the new baseline.  Use a copy instead of
+		// a move to leave the sandbox intact for manual inspection
+		copyFiles(command.CopyAfter, tempDir, "", true)
+	}
+
 	if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
 		return "", fmt.Errorf("sbox command failed with err:\n%s\n%w\n", commandDescription, err)
 	} else if err != nil {
@@ -351,12 +359,13 @@
 	return missingOutputErrors
 }
 
-// copyFiles copies files in or out of the sandbox.
-func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error {
+// copyFiles copies files in or out of the sandbox.  If allowFromNotExists is true then errors
+// caused by a from path not existing are ignored.
+func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, allowFromNotExists bool) error {
 	for _, copyPair := range copies {
 		fromPath := joinPath(fromDir, copyPair.GetFrom())
 		toPath := joinPath(toDir, copyPair.GetTo())
-		err := copyOneFile(fromPath, toPath, copyPair.GetExecutable())
+		err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), allowFromNotExists)
 		if err != nil {
 			return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
 		}
@@ -364,8 +373,9 @@
 	return nil
 }
 
-// copyOneFile copies a file.
-func copyOneFile(from string, to string, executable bool) error {
+// copyOneFile copies a file and its permissions.  If forceExecutable is true it adds u+x to the
+// permissions.  If allowFromNotExists is true it returns nil if the from path doesn't exist.
+func copyOneFile(from string, to string, forceExecutable, allowFromNotExists bool) error {
 	err := os.MkdirAll(filepath.Dir(to), 0777)
 	if err != nil {
 		return err
@@ -373,11 +383,14 @@
 
 	stat, err := os.Stat(from)
 	if err != nil {
+		if os.IsNotExist(err) && allowFromNotExists {
+			return nil
+		}
 		return err
 	}
 
 	perm := stat.Mode()
-	if executable {
+	if forceExecutable {
 		perm = perm | 0100 // u+x
 	}
 
@@ -454,7 +467,7 @@
 		to := applyPathMappings(rspFile.PathMappings, from)
 
 		// Copy the file into the sandbox.
-		err := copyOneFile(from, joinPath(toDir, to), false)
+		err := copyOneFile(from, joinPath(toDir, to), false, false)
 		if err != nil {
 			return err
 		}