blob: 7057b33f047e9f6b9cd445c1ccef62b2dbfb1a79 [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 (
Dan Willemsenc89b6f12019-08-29 14:47:40 -070018 "bytes"
Jeff Gaston90cfb092017-09-26 16:46:10 -070019 "errors"
Jeff Gaston93f0f372017-11-01 13:33:02 -070020 "flag"
Jeff Gastonefc1b412017-03-29 17:29:06 -070021 "fmt"
22 "io/ioutil"
23 "os"
24 "os/exec"
25 "path"
26 "path/filepath"
27 "strings"
Colin Crossd1c1e6f2019-03-29 13:54:39 -070028 "time"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070029
30 "android/soong/makedeps"
Jeff Gastonefc1b412017-03-29 17:29:06 -070031)
32
Jeff Gaston93f0f372017-11-01 13:33:02 -070033var (
34 sandboxesRoot string
35 rawCommand string
36 outputRoot string
37 keepOutDir bool
38 depfileOut string
39)
40
41func init() {
42 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
43 "root of temp directory to put the sandbox into")
44 flag.StringVar(&rawCommand, "c", "",
45 "command to run")
46 flag.StringVar(&outputRoot, "output-root", "",
47 "root of directory to copy outputs into")
48 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
49 "whether to keep the sandbox directory when done")
50
51 flag.StringVar(&depfileOut, "depfile-out", "",
52 "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 -080053
Jeff Gaston93f0f372017-11-01 13:33:02 -070054}
55
56func usageViolation(violation string) {
57 if violation != "" {
58 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
59 }
60
61 fmt.Fprintf(os.Stderr,
Dan Willemsen633c5022019-04-12 11:11:38 -070062 "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+
Jeff Gaston93f0f372017-11-01 13:33:02 -070063 "\n"+
Jeff Gaston8a88db52017-11-06 13:33:14 -080064 "Deletes <outputRoot>,"+
65 "runs <commandToRun>,"+
66 "and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070067
68 flag.PrintDefaults()
69
70 os.Exit(1)
71}
72
Jeff Gastonefc1b412017-03-29 17:29:06 -070073func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070074 flag.Usage = func() {
75 usageViolation("")
76 }
77 flag.Parse()
78
Jeff Gastonefc1b412017-03-29 17:29:06 -070079 error := run()
80 if error != nil {
81 fmt.Fprintln(os.Stderr, error)
82 os.Exit(1)
83 }
84}
85
Jeff Gaston90cfb092017-09-26 16:46:10 -070086func findAllFilesUnder(root string) (paths []string) {
87 paths = []string{}
88 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
89 if !info.IsDir() {
90 relPath, err := filepath.Rel(root, path)
91 if err != nil {
92 // couldn't find relative path from ancestor?
93 panic(err)
94 }
95 paths = append(paths, relPath)
96 }
97 return nil
98 })
99 return paths
100}
101
Jeff Gastonefc1b412017-03-29 17:29:06 -0700102func run() error {
Jeff Gaston02a684b2017-10-27 14:59:27 -0700103 if rawCommand == "" {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700104 usageViolation("-c <commandToRun> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700105 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700106 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700107 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
108 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
109 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
110 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
111 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700112 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700113 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700114 if len(outputRoot) == 0 {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700115 usageViolation("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700116 }
117
Jeff Gaston93f0f372017-11-01 13:33:02 -0700118 // the contents of the __SBOX_OUT_FILES__ variable
119 outputsVarEntries := flag.Args()
120 if len(outputsVarEntries) == 0 {
121 usageViolation("at least one output file must be given")
122 }
123
124 // all outputs
125 var allOutputs []string
126
Jeff Gaston8a88db52017-11-06 13:33:14 -0800127 // setup directories
128 err := os.MkdirAll(sandboxesRoot, 0777)
129 if err != nil {
130 return err
131 }
132 err = os.RemoveAll(outputRoot)
133 if err != nil {
134 return err
135 }
136 err = os.MkdirAll(outputRoot, 0777)
137 if err != nil {
138 return err
139 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700140
141 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700142
Jeff Gaston02a684b2017-10-27 14:59:27 -0700143 for i, filePath := range outputsVarEntries {
Colin Crossbaccf5b2018-02-21 14:07:48 -0800144 if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") {
145 return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700146 }
Colin Crossbaccf5b2018-02-21 14:07:48 -0800147 outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700148 }
149
150 allOutputs = append([]string(nil), outputsVarEntries...)
151
Jeff Gaston93f0f372017-11-01 13:33:02 -0700152 if depfileOut != "" {
153 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700154 if err != nil {
155 return err
156 }
157 allOutputs = append(allOutputs, sandboxedDepfile)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700158 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
159
160 }
161
Jeff Gastonefc1b412017-03-29 17:29:06 -0700162 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
Jeff Gastonf49082a2017-06-07 13:22:22 -0700169 defer func() {
170 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700171 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700172 os.RemoveAll(tempDir)
173 }
174 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700175
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{}
Jeff Gaston02a684b2017-10-27 14:59:27 -0700183 for _, outputPath := range outputsVarEntries {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700184 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
Jeff Gaston02a684b2017-10-27 14:59:27 -0700191 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 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700197 }
198
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700199 commandDescription := rawCommand
200
Jeff Gastonefc1b412017-03-29 17:29:06 -0700201 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()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700206
Jeff Gastonefc1b412017-03-29 17:29:06 -0700207 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700208 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700209 } else if err != nil {
210 return err
211 }
212
Jeff Gastonf49082a2017-06-07 13:22:22 -0700213 // validate that all files are created properly
Jeff Gaston90cfb092017-09-26 16:46:10 -0700214 var missingOutputErrors []string
Jeff Gaston02a684b2017-10-27 14:59:27 -0700215 for _, filePath := range allOutputs {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700216 tempPath := filepath.Join(tempDir, filePath)
217 fileInfo, err := os.Stat(tempPath)
218 if err != nil {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700219 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
Jeff Gastonf49082a2017-06-07 13:22:22 -0700220 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700221 }
222 if fileInfo.IsDir() {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700223 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700224 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700225 }
Jeff Gaston90cfb092017-09-26 16:46:10 -0700226 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
Jeff Gastonf49082a2017-06-07 13:22:22 -0700253 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
254 // Soong will delete it later anyway.
Jeff Gaston93f0f372017-11-01 13:33:02 -0700255 keepOutDir = true
Jeff Gaston90cfb092017-09-26 16:46:10 -0700256 return errors.New(errorMessage)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700257 }
258 // the created files match the declared files; now move them
Jeff Gaston02a684b2017-10-27 14:59:27 -0700259 for _, filePath := range allOutputs {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700260 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700261 destPath := filePath
262 if len(outputRoot) != 0 {
263 destPath = filepath.Join(outputRoot, filePath)
264 }
Jeff Gaston8a88db52017-11-06 13:33:14 -0800265 err := os.MkdirAll(filepath.Dir(destPath), 0777)
266 if err != nil {
267 return err
268 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700269
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
Jeff Gaston8a88db52017-11-06 13:33:14 -0800278 err = os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700279 if err != nil {
280 return err
281 }
282 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700283
Dan Willemsenc89b6f12019-08-29 14:47:40 -0700284 // Rewrite the depfile so that it doesn't include the (randomized) sandbox directory
285 if depfileOut != "" {
286 in, err := ioutil.ReadFile(depfileOut)
287 if err != nil {
288 return err
289 }
290
291 deps, err := makedeps.Parse(depfileOut, bytes.NewBuffer(in))
292 if err != nil {
293 return err
294 }
295
296 deps.Output = "outputfile"
297
298 err = ioutil.WriteFile(depfileOut, deps.Print(), 0666)
299 if err != nil {
300 return err
301 }
302 }
303
Jeff Gastonefc1b412017-03-29 17:29:06 -0700304 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
305 return nil
306}