blob: 10ad5d8b992c040950c98f6d603bb524e7e6aed2 [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
Sam Mortimerf9f96592018-09-18 13:41:03 -070038 copyAllOutput bool
Jeff Gaston93f0f372017-11-01 13:33:02 -070039 depfileOut string
Bill Peckhamc087be12020-02-13 15:55:10 -080040 inputHash string
Jeff Gaston93f0f372017-11-01 13:33:02 -070041)
42
43func init() {
44 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
45 "root of temp directory to put the sandbox into")
46 flag.StringVar(&rawCommand, "c", "",
47 "command to run")
48 flag.StringVar(&outputRoot, "output-root", "",
49 "root of directory to copy outputs into")
50 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
51 "whether to keep the sandbox directory when done")
Sam Mortimerf9f96592018-09-18 13:41:03 -070052 flag.BoolVar(&copyAllOutput, "copy-all-output", false,
53 "whether to copy all output files")
Jeff Gaston93f0f372017-11-01 13:33:02 -070054
55 flag.StringVar(&depfileOut, "depfile-out", "",
56 "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 -080057
Bill Peckhamc087be12020-02-13 15:55:10 -080058 flag.StringVar(&inputHash, "input-hash", "",
59 "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.")
Jeff Gaston93f0f372017-11-01 13:33:02 -070060}
61
62func usageViolation(violation string) {
63 if violation != "" {
64 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
65 }
66
67 fmt.Fprintf(os.Stderr,
Bill Peckhamc087be12020-02-13 15:55:10 -080068 "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> [--depfile-out depFile] [--input-hash hash] <outputFile> [<outputFile>...]\n"+
Jeff Gaston93f0f372017-11-01 13:33:02 -070069 "\n"+
Jeff Gaston8a88db52017-11-06 13:33:14 -080070 "Deletes <outputRoot>,"+
71 "runs <commandToRun>,"+
72 "and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070073
74 flag.PrintDefaults()
75
76 os.Exit(1)
77}
78
Jeff Gastonefc1b412017-03-29 17:29:06 -070079func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070080 flag.Usage = func() {
81 usageViolation("")
82 }
83 flag.Parse()
84
Jeff Gastonefc1b412017-03-29 17:29:06 -070085 error := run()
86 if error != nil {
87 fmt.Fprintln(os.Stderr, error)
88 os.Exit(1)
89 }
90}
91
Jeff Gaston90cfb092017-09-26 16:46:10 -070092func findAllFilesUnder(root string) (paths []string) {
93 paths = []string{}
94 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
95 if !info.IsDir() {
96 relPath, err := filepath.Rel(root, path)
97 if err != nil {
98 // couldn't find relative path from ancestor?
99 panic(err)
100 }
101 paths = append(paths, relPath)
102 }
103 return nil
104 })
105 return paths
106}
107
Jeff Gastonefc1b412017-03-29 17:29:06 -0700108func run() error {
Jeff Gaston02a684b2017-10-27 14:59:27 -0700109 if rawCommand == "" {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700110 usageViolation("-c <commandToRun> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700111 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700112 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700113 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
114 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
115 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
116 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
117 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700118 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700119 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700120 if len(outputRoot) == 0 {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700121 usageViolation("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700122 }
123
Jeff Gaston93f0f372017-11-01 13:33:02 -0700124 // the contents of the __SBOX_OUT_FILES__ variable
125 outputsVarEntries := flag.Args()
Sam Mortimerf9f96592018-09-18 13:41:03 -0700126 if !copyAllOutput && len(outputsVarEntries) == 0 {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700127 usageViolation("at least one output file must be given")
128 }
129
130 // all outputs
131 var allOutputs []string
132
Jeff Gaston8a88db52017-11-06 13:33:14 -0800133 // setup directories
134 err := os.MkdirAll(sandboxesRoot, 0777)
135 if err != nil {
136 return err
137 }
138 err = os.RemoveAll(outputRoot)
139 if err != nil {
140 return err
141 }
142 err = os.MkdirAll(outputRoot, 0777)
143 if err != nil {
144 return err
145 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700146
147 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700148
Jeff Gaston02a684b2017-10-27 14:59:27 -0700149 for i, filePath := range outputsVarEntries {
Colin Crossbaccf5b2018-02-21 14:07:48 -0800150 if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") {
151 return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700152 }
Colin Crossbaccf5b2018-02-21 14:07:48 -0800153 outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700154 }
155
156 allOutputs = append([]string(nil), outputsVarEntries...)
157
Jeff Gaston93f0f372017-11-01 13:33:02 -0700158 if depfileOut != "" {
159 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700160 if err != nil {
161 return err
162 }
163 allOutputs = append(allOutputs, sandboxedDepfile)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700164 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
165
166 }
167
Jeff Gastonefc1b412017-03-29 17:29:06 -0700168 if err != nil {
169 return fmt.Errorf("Failed to create temp dir: %s", err)
170 }
171
172 // In the common case, the following line of code is what removes the sandbox
173 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
174 // then at the beginning of the next build, Soong will retry the cleanup
Jeff Gastonf49082a2017-06-07 13:22:22 -0700175 defer func() {
176 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700177 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700178 os.RemoveAll(tempDir)
179 }
180 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700181
182 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
183 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
184 }
185
186 if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
187 // expands into a space-separated list of output files to be generated into the sandbox directory
188 tempOutPaths := []string{}
Jeff Gaston02a684b2017-10-27 14:59:27 -0700189 for _, outputPath := range outputsVarEntries {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700190 tempOutPath := path.Join(tempDir, outputPath)
191 tempOutPaths = append(tempOutPaths, tempOutPath)
192 }
193 pathsText := strings.Join(tempOutPaths, " ")
194 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
195 }
196
Jeff Gaston02a684b2017-10-27 14:59:27 -0700197 for _, filePath := range allOutputs {
198 dir := path.Join(tempDir, filepath.Dir(filePath))
199 err = os.MkdirAll(dir, 0777)
200 if err != nil {
201 return err
202 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700203 }
204
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700205 commandDescription := rawCommand
206
Jeff Gastonefc1b412017-03-29 17:29:06 -0700207 cmd := exec.Command("bash", "-c", rawCommand)
208 cmd.Stdin = os.Stdin
209 cmd.Stdout = os.Stdout
210 cmd.Stderr = os.Stderr
211 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700212
Jeff Gastonefc1b412017-03-29 17:29:06 -0700213 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700214 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700215 } else if err != nil {
216 return err
217 }
218
Jeff Gastonf49082a2017-06-07 13:22:22 -0700219 // validate that all files are created properly
Jeff Gaston90cfb092017-09-26 16:46:10 -0700220 var missingOutputErrors []string
Jeff Gaston02a684b2017-10-27 14:59:27 -0700221 for _, filePath := range allOutputs {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700222 tempPath := filepath.Join(tempDir, filePath)
223 fileInfo, err := os.Stat(tempPath)
224 if err != nil {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700225 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
Jeff Gastonf49082a2017-06-07 13:22:22 -0700226 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700227 }
228 if fileInfo.IsDir() {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700229 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700230 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700231 }
Sam Mortimerf9f96592018-09-18 13:41:03 -0700232 if !copyAllOutput && len(missingOutputErrors) > 0 {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700233 // find all created files for making a more informative error message
234 createdFiles := findAllFilesUnder(tempDir)
235
236 // build error message
237 errorMessage := "mismatch between declared and actual outputs\n"
238 errorMessage += "in sbox command(" + commandDescription + ")\n\n"
239 errorMessage += "in sandbox " + tempDir + ",\n"
240 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
241 for _, missingOutputError := range missingOutputErrors {
242 errorMessage += " " + missingOutputError + "\n"
243 }
244 if len(createdFiles) < 1 {
245 errorMessage += "created 0 files."
246 } else {
247 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
248 creationMessages := createdFiles
249 maxNumCreationLines := 10
250 if len(creationMessages) > maxNumCreationLines {
251 creationMessages = creationMessages[:maxNumCreationLines]
252 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
253 }
254 for _, creationMessage := range creationMessages {
255 errorMessage += " " + creationMessage + "\n"
256 }
257 }
258
Jeff Gastonf49082a2017-06-07 13:22:22 -0700259 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
260 // Soong will delete it later anyway.
Jeff Gaston93f0f372017-11-01 13:33:02 -0700261 keepOutDir = true
Jeff Gaston90cfb092017-09-26 16:46:10 -0700262 return errors.New(errorMessage)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700263 }
Sam Mortimerf9f96592018-09-18 13:41:03 -0700264 var filePathList []string
265 if copyAllOutput {
266 filePathList = findAllFilesUnder(tempDir)
267 } else {
268 filePathList = allOutputs
269 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700270 // the created files match the declared files; now move them
Sam Mortimerf9f96592018-09-18 13:41:03 -0700271 for _, filePath := range filePathList {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700272 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700273 destPath := filePath
274 if len(outputRoot) != 0 {
275 destPath = filepath.Join(outputRoot, filePath)
276 }
Jeff Gaston8a88db52017-11-06 13:33:14 -0800277 err := os.MkdirAll(filepath.Dir(destPath), 0777)
278 if err != nil {
279 return err
280 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700281
282 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
283 // files with old timestamps).
284 now := time.Now()
285 err = os.Chtimes(tempPath, now, now)
286 if err != nil {
287 return err
288 }
289
Jeff Gaston8a88db52017-11-06 13:33:14 -0800290 err = os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700291 if err != nil {
292 return err
293 }
294 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700295
Dan Willemsenc89b6f12019-08-29 14:47:40 -0700296 // Rewrite the depfile so that it doesn't include the (randomized) sandbox directory
297 if depfileOut != "" {
298 in, err := ioutil.ReadFile(depfileOut)
299 if err != nil {
300 return err
301 }
302
303 deps, err := makedeps.Parse(depfileOut, bytes.NewBuffer(in))
304 if err != nil {
305 return err
306 }
307
308 deps.Output = "outputfile"
309
310 err = ioutil.WriteFile(depfileOut, deps.Print(), 0666)
311 if err != nil {
312 return err
313 }
314 }
315
Jeff Gastonefc1b412017-03-29 17:29:06 -0700316 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
317 return nil
318}