Have Soong try to enforce that genrules declare all their outputs.

This causes Soong to put the outputs of each genrule into a temporary
location and copy the declared outputs back to the output directory.
This gets the process closer to having an actual sandbox.

Bug: 35562758
Test: make

Change-Id: I8048fbf1a3899a86fb99d71b60669b6633b07b3e
diff --git a/cmd/sbox/sbox.go b/cmd/sbox/sbox.go
new file mode 100644
index 0000000..ead3443
--- /dev/null
+++ b/cmd/sbox/sbox.go
@@ -0,0 +1,133 @@
+// Copyright 2017 Google Inc. 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.
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"strings"
+)
+
+func main() {
+	error := run()
+	if error != nil {
+		fmt.Fprintln(os.Stderr, error)
+		os.Exit(1)
+	}
+}
+
+var usage = "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> <outputFiles>"
+
+func usageError(violation string) error {
+	return fmt.Errorf("Usage error: %s.\n %s", violation, usage)
+}
+
+func run() error {
+	var outFiles []string
+	args := os.Args[1:]
+
+	var rawCommand string
+	var sandboxesRoot string
+
+	for i := 0; i < len(args); i++ {
+		arg := args[i]
+		if arg == "--sandbox-path" {
+			sandboxesRoot = args[i+1]
+			i++
+		} else if arg == "-c" {
+			rawCommand = args[i+1]
+			i++
+		} else {
+			outFiles = append(outFiles, arg)
+		}
+	}
+	if len(rawCommand) == 0 {
+		return usageError("-c <commandToRun> is required and must be non-empty")
+	}
+	if outFiles == nil {
+		return usageError("at least one output file must be given")
+	}
+	if len(sandboxesRoot) == 0 {
+		// In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
+		// and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
+		// the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
+		// However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
+		// and by passing it as a parameter we don't need to duplicate its value
+		return usageError("--sandbox-path <sandboxPath> is required and must be non-empty")
+	}
+
+	os.MkdirAll(sandboxesRoot, 0777)
+
+	tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
+	if err != nil {
+		return fmt.Errorf("Failed to create temp dir: %s", err)
+	}
+
+	// In the common case, the following line of code is what removes the sandbox
+	// If a fatal error occurs (such as if our Go process is killed unexpectedly),
+	// then at the beginning of the next build, Soong will retry the cleanup
+	defer os.RemoveAll(tempDir)
+
+	if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
+		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
+	}
+
+	if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
+		// expands into a space-separated list of output files to be generated into the sandbox directory
+		tempOutPaths := []string{}
+		for _, outputPath := range outFiles {
+			tempOutPath := path.Join(tempDir, outputPath)
+			tempOutPaths = append(tempOutPaths, tempOutPath)
+		}
+		pathsText := strings.Join(tempOutPaths, " ")
+		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
+	}
+
+	for _, filePath := range outFiles {
+		os.MkdirAll(path.Join(tempDir, filepath.Dir(filePath)), 0777)
+	}
+
+	cmd := exec.Command("bash", "-c", rawCommand)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err = cmd.Run()
+	if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
+		return fmt.Errorf("sbox command %#v failed with err %#v\n", cmd, err)
+	} else if err != nil {
+		return err
+	}
+
+	for _, filePath := range outFiles {
+		tempPath := filepath.Join(tempDir, filePath)
+		fileInfo, err := os.Stat(tempPath)
+		if err != nil {
+			return fmt.Errorf("command run under sbox did not create expected output file %s", filePath)
+		}
+		if fileInfo.IsDir() {
+			return fmt.Errorf("Output path %s refers to a directory, not a file. This is not permitted because it prevents robust up-to-date checks", filePath)
+		}
+		err = os.Rename(tempPath, filePath)
+		if err != nil {
+			return err
+		}
+	}
+	// TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
+	return nil
+}