• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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	"errors"
19	"flag"
20	"fmt"
21	"io/ioutil"
22	"os"
23	"os/exec"
24	"path"
25	"path/filepath"
26	"strings"
27	"time"
28)
29
30var (
31	sandboxesRoot string
32	rawCommand    string
33	outputRoot    string
34	keepOutDir    bool
35	depfileOut    string
36)
37
38func init() {
39	flag.StringVar(&sandboxesRoot, "sandbox-path", "",
40		"root of temp directory to put the sandbox into")
41	flag.StringVar(&rawCommand, "c", "",
42		"command to run")
43	flag.StringVar(&outputRoot, "output-root", "",
44		"root of directory to copy outputs into")
45	flag.BoolVar(&keepOutDir, "keep-out-dir", false,
46		"whether to keep the sandbox directory when done")
47
48	flag.StringVar(&depfileOut, "depfile-out", "",
49		"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__")
50
51}
52
53func usageViolation(violation string) {
54	if violation != "" {
55		fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
56	}
57
58	fmt.Fprintf(os.Stderr,
59		"Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> --overwrite [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+
60			"\n"+
61			"Deletes <outputRoot>,"+
62			"runs <commandToRun>,"+
63			"and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
64
65	flag.PrintDefaults()
66
67	os.Exit(1)
68}
69
70func main() {
71	flag.Usage = func() {
72		usageViolation("")
73	}
74	flag.Parse()
75
76	error := run()
77	if error != nil {
78		fmt.Fprintln(os.Stderr, error)
79		os.Exit(1)
80	}
81}
82
83func findAllFilesUnder(root string) (paths []string) {
84	paths = []string{}
85	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
86		if !info.IsDir() {
87			relPath, err := filepath.Rel(root, path)
88			if err != nil {
89				// couldn't find relative path from ancestor?
90				panic(err)
91			}
92			paths = append(paths, relPath)
93		}
94		return nil
95	})
96	return paths
97}
98
99func run() error {
100	if rawCommand == "" {
101		usageViolation("-c <commandToRun> is required and must be non-empty")
102	}
103	if sandboxesRoot == "" {
104		// In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
105		// and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
106		// the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
107		// However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
108		// and by passing it as a parameter we don't need to duplicate its value
109		usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
110	}
111	if len(outputRoot) == 0 {
112		usageViolation("--output-root <outputRoot> is required and must be non-empty")
113	}
114
115	// the contents of the __SBOX_OUT_FILES__ variable
116	outputsVarEntries := flag.Args()
117	if len(outputsVarEntries) == 0 {
118		usageViolation("at least one output file must be given")
119	}
120
121	// all outputs
122	var allOutputs []string
123
124	// setup directories
125	err := os.MkdirAll(sandboxesRoot, 0777)
126	if err != nil {
127		return err
128	}
129	err = os.RemoveAll(outputRoot)
130	if err != nil {
131		return err
132	}
133	err = os.MkdirAll(outputRoot, 0777)
134	if err != nil {
135		return err
136	}
137
138	tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
139
140	for i, filePath := range outputsVarEntries {
141		if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") {
142			return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`")
143		}
144		outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/")
145	}
146
147	allOutputs = append([]string(nil), outputsVarEntries...)
148
149	if depfileOut != "" {
150		sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
151		if err != nil {
152			return err
153		}
154		allOutputs = append(allOutputs, sandboxedDepfile)
155		if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") {
156			return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__")
157		}
158		rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
159
160	}
161
162	if err != nil {
163		return fmt.Errorf("Failed to create temp dir: %s", err)
164	}
165
166	// In the common case, the following line of code is what removes the sandbox
167	// If a fatal error occurs (such as if our Go process is killed unexpectedly),
168	// then at the beginning of the next build, Soong will retry the cleanup
169	defer func() {
170		// in some cases we decline to remove the temp dir, to facilitate debugging
171		if !keepOutDir {
172			os.RemoveAll(tempDir)
173		}
174	}()
175
176	if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
177		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
178	}
179
180	if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
181		// expands into a space-separated list of output files to be generated into the sandbox directory
182		tempOutPaths := []string{}
183		for _, outputPath := range outputsVarEntries {
184			tempOutPath := path.Join(tempDir, outputPath)
185			tempOutPaths = append(tempOutPaths, tempOutPath)
186		}
187		pathsText := strings.Join(tempOutPaths, " ")
188		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
189	}
190
191	for _, filePath := range allOutputs {
192		dir := path.Join(tempDir, filepath.Dir(filePath))
193		err = os.MkdirAll(dir, 0777)
194		if err != nil {
195			return err
196		}
197	}
198
199	commandDescription := rawCommand
200
201	cmd := exec.Command("bash", "-c", rawCommand)
202	cmd.Stdin = os.Stdin
203	cmd.Stdout = os.Stdout
204	cmd.Stderr = os.Stderr
205	err = cmd.Run()
206
207	if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
208		return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
209	} else if err != nil {
210		return err
211	}
212
213	// validate that all files are created properly
214	var missingOutputErrors []string
215	for _, filePath := range allOutputs {
216		tempPath := filepath.Join(tempDir, filePath)
217		fileInfo, err := os.Stat(tempPath)
218		if err != nil {
219			missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
220			continue
221		}
222		if fileInfo.IsDir() {
223			missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
224		}
225	}
226	if len(missingOutputErrors) > 0 {
227		// find all created files for making a more informative error message
228		createdFiles := findAllFilesUnder(tempDir)
229
230		// build error message
231		errorMessage := "mismatch between declared and actual outputs\n"
232		errorMessage += "in sbox command(" + commandDescription + ")\n\n"
233		errorMessage += "in sandbox " + tempDir + ",\n"
234		errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
235		for _, missingOutputError := range missingOutputErrors {
236			errorMessage += "  " + missingOutputError + "\n"
237		}
238		if len(createdFiles) < 1 {
239			errorMessage += "created 0 files."
240		} else {
241			errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
242			creationMessages := createdFiles
243			maxNumCreationLines := 10
244			if len(creationMessages) > maxNumCreationLines {
245				creationMessages = creationMessages[:maxNumCreationLines]
246				creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
247			}
248			for _, creationMessage := range creationMessages {
249				errorMessage += "  " + creationMessage + "\n"
250			}
251		}
252
253		// Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
254		// Soong will delete it later anyway.
255		keepOutDir = true
256		return errors.New(errorMessage)
257	}
258	// the created files match the declared files; now move them
259	for _, filePath := range allOutputs {
260		tempPath := filepath.Join(tempDir, filePath)
261		destPath := filePath
262		if len(outputRoot) != 0 {
263			destPath = filepath.Join(outputRoot, filePath)
264		}
265		err := os.MkdirAll(filepath.Dir(destPath), 0777)
266		if err != nil {
267			return err
268		}
269
270		// Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
271		// files with old timestamps).
272		now := time.Now()
273		err = os.Chtimes(tempPath, now, now)
274		if err != nil {
275			return err
276		}
277
278		err = os.Rename(tempPath, destPath)
279		if err != nil {
280			return err
281		}
282	}
283
284	// TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
285	return nil
286}
287