blob: 3b41c908803d967b2ed4d5f7494f967f78d4a334 [file] [log] [blame]
Jeff Gastonefc1b412017-03-29 17:29:06 -07001// 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 (
Jeff Gaston90cfb092017-09-26 16:46:10 -070018 "errors"
Jeff Gaston93f0f372017-11-01 13:33:02 -070019 "flag"
Jeff Gastonefc1b412017-03-29 17:29:06 -070020 "fmt"
21 "io/ioutil"
22 "os"
23 "os/exec"
24 "path"
25 "path/filepath"
26 "strings"
27)
28
Jeff Gaston93f0f372017-11-01 13:33:02 -070029var (
30 sandboxesRoot string
31 rawCommand string
32 outputRoot string
33 keepOutDir bool
34 depfileOut string
35)
36
37func init() {
38 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
39 "root of temp directory to put the sandbox into")
40 flag.StringVar(&rawCommand, "c", "",
41 "command to run")
42 flag.StringVar(&outputRoot, "output-root", "",
43 "root of directory to copy outputs into")
44 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
45 "whether to keep the sandbox directory when done")
46
47 flag.StringVar(&depfileOut, "depfile-out", "",
48 "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__")
Jeff Gaston8a88db52017-11-06 13:33:14 -080049
Jeff Gaston93f0f372017-11-01 13:33:02 -070050}
51
52func usageViolation(violation string) {
53 if violation != "" {
54 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
55 }
56
57 fmt.Fprintf(os.Stderr,
Jeff Gaston8a88db52017-11-06 13:33:14 -080058 "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> --overwrite [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+
Jeff Gaston93f0f372017-11-01 13:33:02 -070059 "\n"+
Jeff Gaston8a88db52017-11-06 13:33:14 -080060 "Deletes <outputRoot>,"+
61 "runs <commandToRun>,"+
62 "and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070063
64 flag.PrintDefaults()
65
66 os.Exit(1)
67}
68
Jeff Gastonefc1b412017-03-29 17:29:06 -070069func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070070 flag.Usage = func() {
71 usageViolation("")
72 }
73 flag.Parse()
74
Jeff Gastonefc1b412017-03-29 17:29:06 -070075 error := run()
76 if error != nil {
77 fmt.Fprintln(os.Stderr, error)
78 os.Exit(1)
79 }
80}
81
Jeff Gaston90cfb092017-09-26 16:46:10 -070082func findAllFilesUnder(root string) (paths []string) {
83 paths = []string{}
84 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
85 if !info.IsDir() {
86 relPath, err := filepath.Rel(root, path)
87 if err != nil {
88 // couldn't find relative path from ancestor?
89 panic(err)
90 }
91 paths = append(paths, relPath)
92 }
93 return nil
94 })
95 return paths
96}
97
Jeff Gastonefc1b412017-03-29 17:29:06 -070098func run() error {
Jeff Gaston02a684b2017-10-27 14:59:27 -070099 if rawCommand == "" {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700100 usageViolation("-c <commandToRun> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700101 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700102 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700103 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
104 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
105 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
106 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
107 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700108 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700109 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700110 if len(outputRoot) == 0 {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700111 usageViolation("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700112 }
113
Jeff Gaston93f0f372017-11-01 13:33:02 -0700114 // the contents of the __SBOX_OUT_FILES__ variable
115 outputsVarEntries := flag.Args()
116 if len(outputsVarEntries) == 0 {
117 usageViolation("at least one output file must be given")
118 }
119
120 // all outputs
121 var allOutputs []string
122
Jeff Gaston8a88db52017-11-06 13:33:14 -0800123 // setup directories
124 err := os.MkdirAll(sandboxesRoot, 0777)
125 if err != nil {
126 return err
127 }
128 err = os.RemoveAll(outputRoot)
129 if err != nil {
130 return err
131 }
132 err = os.MkdirAll(outputRoot, 0777)
133 if err != nil {
134 return err
135 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700136
137 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700138
139 // Rewrite output file paths to be relative to output root
140 // This facilitates matching them up against the corresponding paths in the temporary directory in case they're absolute
141 for i, filePath := range outputsVarEntries {
142 relativePath, err := filepath.Rel(outputRoot, filePath)
143 if err != nil {
144 return err
145 }
146 outputsVarEntries[i] = relativePath
147 }
148
149 allOutputs = append([]string(nil), outputsVarEntries...)
150
Jeff Gaston93f0f372017-11-01 13:33:02 -0700151 if depfileOut != "" {
152 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700153 if err != nil {
154 return err
155 }
156 allOutputs = append(allOutputs, sandboxedDepfile)
157 if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") {
158 return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__")
159 }
160 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
161
162 }
163
Jeff Gastonefc1b412017-03-29 17:29:06 -0700164 if err != nil {
165 return fmt.Errorf("Failed to create temp dir: %s", err)
166 }
167
168 // In the common case, the following line of code is what removes the sandbox
169 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
170 // then at the beginning of the next build, Soong will retry the cleanup
Jeff Gastonf49082a2017-06-07 13:22:22 -0700171 defer func() {
172 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700173 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700174 os.RemoveAll(tempDir)
175 }
176 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700177
178 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
179 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
180 }
181
182 if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
183 // expands into a space-separated list of output files to be generated into the sandbox directory
184 tempOutPaths := []string{}
Jeff Gaston02a684b2017-10-27 14:59:27 -0700185 for _, outputPath := range outputsVarEntries {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700186 tempOutPath := path.Join(tempDir, outputPath)
187 tempOutPaths = append(tempOutPaths, tempOutPath)
188 }
189 pathsText := strings.Join(tempOutPaths, " ")
190 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
191 }
192
Jeff Gaston02a684b2017-10-27 14:59:27 -0700193 for _, filePath := range allOutputs {
194 dir := path.Join(tempDir, filepath.Dir(filePath))
195 err = os.MkdirAll(dir, 0777)
196 if err != nil {
197 return err
198 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700199 }
200
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700201 commandDescription := rawCommand
202
Jeff Gastonefc1b412017-03-29 17:29:06 -0700203 cmd := exec.Command("bash", "-c", rawCommand)
204 cmd.Stdin = os.Stdin
205 cmd.Stdout = os.Stdout
206 cmd.Stderr = os.Stderr
207 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700208
Jeff Gastonefc1b412017-03-29 17:29:06 -0700209 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700210 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700211 } else if err != nil {
212 return err
213 }
214
Jeff Gastonf49082a2017-06-07 13:22:22 -0700215 // validate that all files are created properly
Jeff Gaston90cfb092017-09-26 16:46:10 -0700216 var missingOutputErrors []string
Jeff Gaston02a684b2017-10-27 14:59:27 -0700217 for _, filePath := range allOutputs {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700218 tempPath := filepath.Join(tempDir, filePath)
219 fileInfo, err := os.Stat(tempPath)
220 if err != nil {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700221 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
Jeff Gastonf49082a2017-06-07 13:22:22 -0700222 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700223 }
224 if fileInfo.IsDir() {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700225 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700226 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700227 }
Jeff Gaston90cfb092017-09-26 16:46:10 -0700228 if len(missingOutputErrors) > 0 {
229 // find all created files for making a more informative error message
230 createdFiles := findAllFilesUnder(tempDir)
231
232 // build error message
233 errorMessage := "mismatch between declared and actual outputs\n"
234 errorMessage += "in sbox command(" + commandDescription + ")\n\n"
235 errorMessage += "in sandbox " + tempDir + ",\n"
236 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
237 for _, missingOutputError := range missingOutputErrors {
238 errorMessage += " " + missingOutputError + "\n"
239 }
240 if len(createdFiles) < 1 {
241 errorMessage += "created 0 files."
242 } else {
243 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
244 creationMessages := createdFiles
245 maxNumCreationLines := 10
246 if len(creationMessages) > maxNumCreationLines {
247 creationMessages = creationMessages[:maxNumCreationLines]
248 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
249 }
250 for _, creationMessage := range creationMessages {
251 errorMessage += " " + creationMessage + "\n"
252 }
253 }
254
Jeff Gastonf49082a2017-06-07 13:22:22 -0700255 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
256 // Soong will delete it later anyway.
Jeff Gaston93f0f372017-11-01 13:33:02 -0700257 keepOutDir = true
Jeff Gaston90cfb092017-09-26 16:46:10 -0700258 return errors.New(errorMessage)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700259 }
260 // the created files match the declared files; now move them
Jeff Gaston02a684b2017-10-27 14:59:27 -0700261 for _, filePath := range allOutputs {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700262 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700263 destPath := filePath
264 if len(outputRoot) != 0 {
265 destPath = filepath.Join(outputRoot, filePath)
266 }
Jeff Gaston8a88db52017-11-06 13:33:14 -0800267 err := os.MkdirAll(filepath.Dir(destPath), 0777)
268 if err != nil {
269 return err
270 }
271 err = os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700272 if err != nil {
273 return err
274 }
275 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700276
Jeff Gastonefc1b412017-03-29 17:29:06 -0700277 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
278 return nil
279}