1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"bytes"
19	"errors"
20	"flag"
21	"fmt"
22	"io/ioutil"
23	"os"
24	"os/exec"
25	"path"
26	"path/filepath"
27	"strings"
28	"time"
29
30	"android/soong/makedeps"
31)
32
33var (
34	sandboxesRoot string
35	rawCommand    string
36	outputRoot    string
37	keepOutDir    bool
38	depfileOut    string
39	inputHash     string
40)
41
42func init() {
43	flag.StringVar(&sandboxesRoot, "sandbox-path", "",
44		"root of temp directory to put the sandbox into")
45	flag.StringVar(&rawCommand, "c", "",
46		"command to run")
47	flag.StringVar(&outputRoot, "output-root", "",
48		"root of directory to copy outputs into")
49	flag.BoolVar(&keepOutDir, "keep-out-dir", false,
50		"whether to keep the sandbox directory when done")
51
52	flag.StringVar(&depfileOut, "depfile-out", "",
53		"file path of the depfile to generate. This value will replace '__SBOX_DEPFILE__' in the command and will be treated as an output but won't be added to __SBOX_OUT_FILES__")
54
55	flag.StringVar(&inputHash, "input-hash", "",
56		"This option is ignored. Typical usage is to supply a hash of the list of input names so that the module will be rebuilt if the list (and thus the hash) changes.")
57}
58
59func usageViolation(violation string) {
60	if violation != "" {
61		fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
62	}
63
64	fmt.Fprintf(os.Stderr,
65		"Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> [--depfile-out depFile] [--input-hash hash] <outputFile> [<outputFile>...]\n"+
66			"\n"+
67			"Deletes <outputRoot>,"+
68			"runs <commandToRun>,"+
69			"and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
70
71	flag.PrintDefaults()
72
73	os.Exit(1)
74}
75
76func main() {
77	flag.Usage = func() {
78		usageViolation("")
79	}
80	flag.Parse()
81
82	error := run()
83	if error != nil {
84		fmt.Fprintln(os.Stderr, error)
85		os.Exit(1)
86	}
87}
88
89func findAllFilesUnder(root string) (paths []string) {
90	paths = []string{}
91	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
92		if !info.IsDir() {
93			relPath, err := filepath.Rel(root, path)
94			if err != nil {
95				// couldn't find relative path from ancestor?
96				panic(err)
97			}
98			paths = append(paths, relPath)
99		}
100		return nil
101	})
102	return paths
103}
104
105func run() error {
106	if rawCommand == "" {
107		usageViolation("-c <commandToRun> is required and must be non-empty")
108	}
109	if sandboxesRoot == "" {
110		// In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
111		// and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
112		// the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
113		// However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
114		// and by passing it as a parameter we don't need to duplicate its value
115		usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
116	}
117	if len(outputRoot) == 0 {
118		usageViolation("--output-root <outputRoot> is required and must be non-empty")
119	}
120
121	// the contents of the __SBOX_OUT_FILES__ variable
122	outputsVarEntries := flag.Args()
123	if len(outputsVarEntries) == 0 {
124		usageViolation("at least one output file must be given")
125	}
126
127	// all outputs
128	var allOutputs []string
129
130	// setup directories
131	err := os.MkdirAll(sandboxesRoot, 0777)
132	if err != nil {
133		return err
134	}
135	err = os.RemoveAll(outputRoot)
136	if err != nil {
137		return err
138	}
139	err = os.MkdirAll(outputRoot, 0777)
140	if err != nil {
141		return err
142	}
143
144	tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
145
146	for i, filePath := range outputsVarEntries {
147		if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") {
148			return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`")
149		}
150		outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/")
151	}
152
153	allOutputs = append([]string(nil), outputsVarEntries...)
154
155	if depfileOut != "" {
156		sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
157		if err != nil {
158			return err
159		}
160		allOutputs = append(allOutputs, sandboxedDepfile)
161		rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
162
163	}
164
165	if err != nil {
166		return fmt.Errorf("Failed to create temp dir: %s", err)
167	}
168
169	// In the common case, the following line of code is what removes the sandbox
170	// If a fatal error occurs (such as if our Go process is killed unexpectedly),
171	// then at the beginning of the next build, Soong will retry the cleanup
172	defer func() {
173		// in some cases we decline to remove the temp dir, to facilitate debugging
174		if !keepOutDir {
175			os.RemoveAll(tempDir)
176		}
177	}()
178
179	if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
180		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
181	}
182
183	if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
184		// expands into a space-separated list of output files to be generated into the sandbox directory
185		tempOutPaths := []string{}
186		for _, outputPath := range outputsVarEntries {
187			tempOutPath := path.Join(tempDir, outputPath)
188			tempOutPaths = append(tempOutPaths, tempOutPath)
189		}
190		pathsText := strings.Join(tempOutPaths, " ")
191		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
192	}
193
194	for _, filePath := range allOutputs {
195		dir := path.Join(tempDir, filepath.Dir(filePath))
196		err = os.MkdirAll(dir, 0777)
197		if err != nil {
198			return err
199		}
200	}
201
202	commandDescription := rawCommand
203
204	cmd := exec.Command("bash", "-c", rawCommand)
205	cmd.Stdin = os.Stdin
206	cmd.Stdout = os.Stdout
207	cmd.Stderr = os.Stderr
208	err = cmd.Run()
209
210	if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
211		return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
212	} else if err != nil {
213		return err
214	}
215
216	// validate that all files are created properly
217	var missingOutputErrors []string
218	for _, filePath := range allOutputs {
219		tempPath := filepath.Join(tempDir, filePath)
220		fileInfo, err := os.Stat(tempPath)
221		if err != nil {
222			missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
223			continue
224		}
225		if fileInfo.IsDir() {
226			missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
227		}
228	}
229	if len(missingOutputErrors) > 0 {
230		// find all created files for making a more informative error message
231		createdFiles := findAllFilesUnder(tempDir)
232
233		// build error message
234		errorMessage := "mismatch between declared and actual outputs\n"
235		errorMessage += "in sbox command(" + commandDescription + ")\n\n"
236		errorMessage += "in sandbox " + tempDir + ",\n"
237		errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
238		for _, missingOutputError := range missingOutputErrors {
239			errorMessage += "  " + missingOutputError + "\n"
240		}
241		if len(createdFiles) < 1 {
242			errorMessage += "created 0 files."
243		} else {
244			errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
245			creationMessages := createdFiles
246			maxNumCreationLines := 10
247			if len(creationMessages) > maxNumCreationLines {
248				creationMessages = creationMessages[:maxNumCreationLines]
249				creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
250			}
251			for _, creationMessage := range creationMessages {
252				errorMessage += "  " + creationMessage + "\n"
253			}
254		}
255
256		// Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
257		// Soong will delete it later anyway.
258		keepOutDir = true
259		return errors.New(errorMessage)
260	}
261	// the created files match the declared files; now move them
262	for _, filePath := range allOutputs {
263		tempPath := filepath.Join(tempDir, filePath)
264		destPath := filePath
265		if len(outputRoot) != 0 {
266			destPath = filepath.Join(outputRoot, filePath)
267		}
268		err := os.MkdirAll(filepath.Dir(destPath), 0777)
269		if err != nil {
270			return err
271		}
272
273		// Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
274		// files with old timestamps).
275		now := time.Now()
276		err = os.Chtimes(tempPath, now, now)
277		if err != nil {
278			return err
279		}
280
281		err = os.Rename(tempPath, destPath)
282		if err != nil {
283			return err
284		}
285	}
286
287	// Rewrite the depfile so that it doesn't include the (randomized) sandbox directory
288	if depfileOut != "" {
289		in, err := ioutil.ReadFile(depfileOut)
290		if err != nil {
291			return err
292		}
293
294		deps, err := makedeps.Parse(depfileOut, bytes.NewBuffer(in))
295		if err != nil {
296			return err
297		}
298
299		deps.Output = "outputfile"
300
301		err = ioutil.WriteFile(depfileOut, deps.Print(), 0666)
302		if err != nil {
303			return err
304		}
305	}
306
307	// TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
308	return nil
309}
310