blob: 65a34fdf4278a5f46ef150e772598b7b84141c23 [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"
Colin Cross619b9ab2020-11-20 18:44:31 +000025 "path"
Jeff Gastonefc1b412017-03-29 17:29:06 -070026 "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
Colin Cross619b9ab2020-11-20 18:44:31 +000035 rawCommand string
36 outputRoot string
Jeff Gaston93f0f372017-11-01 13:33:02 -070037 keepOutDir bool
Colin Cross619b9ab2020-11-20 18:44:31 +000038 depfileOut string
39 inputHash string
Jeff Gaston93f0f372017-11-01 13:33:02 -070040)
41
42func init() {
43 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
44 "root of temp directory to put the sandbox into")
Colin Cross619b9ab2020-11-20 18:44:31 +000045 flag.StringVar(&rawCommand, "c", "",
46 "command to run")
47 flag.StringVar(&outputRoot, "output-root", "",
48 "root of directory to copy outputs into")
Jeff Gaston93f0f372017-11-01 13:33:02 -070049 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
50 "whether to keep the sandbox directory when done")
Colin Cross619b9ab2020-11-20 18:44:31 +000051
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.")
Jeff Gaston93f0f372017-11-01 13:33:02 -070057}
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,
Colin Cross619b9ab2020-11-20 18:44:31 +000065 "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")
Jeff Gaston93f0f372017-11-01 13:33:02 -070070
71 flag.PrintDefaults()
72
73 os.Exit(1)
74}
75
Jeff Gastonefc1b412017-03-29 17:29:06 -070076func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070077 flag.Usage = func() {
78 usageViolation("")
79 }
80 flag.Parse()
81
Jeff Gastonefc1b412017-03-29 17:29:06 -070082 error := run()
83 if error != nil {
84 fmt.Fprintln(os.Stderr, error)
85 os.Exit(1)
86 }
87}
88
Jeff Gaston90cfb092017-09-26 16:46:10 -070089func 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
Jeff Gastonefc1b412017-03-29 17:29:06 -0700105func run() error {
Colin Cross619b9ab2020-11-20 18:44:31 +0000106 if rawCommand == "" {
107 usageViolation("-c <commandToRun> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700108 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700109 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700110 // 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
Jeff Gaston93f0f372017-11-01 13:33:02 -0700115 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700116 }
Colin Cross619b9ab2020-11-20 18:44:31 +0000117 if len(outputRoot) == 0 {
118 usageViolation("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700119 }
120
Colin Cross619b9ab2020-11-20 18:44:31 +0000121 // 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)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800132 if err != nil {
Colin Cross619b9ab2020-11-20 18:44:31 +0000133 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
Jeff Gaston8a88db52017-11-06 13:33:14 -0800142 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700143
144 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Colin Cross619b9ab2020-11-20 18:44:31 +0000145
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
Jeff Gastonefc1b412017-03-29 17:29:06 -0700165 if err != nil {
Colin Cross619b9ab2020-11-20 18:44:31 +0000166 return fmt.Errorf("Failed to create temp dir: %s", err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700167 }
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),
Colin Cross619b9ab2020-11-20 18:44:31 +0000171 // then at the beginning of the next build, Soong will retry the cleanup
Jeff Gastonf49082a2017-06-07 13:22:22 -0700172 defer func() {
173 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700174 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700175 os.RemoveAll(tempDir)
176 }
177 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700178
Colin Cross619b9ab2020-11-20 18:44:31 +0000179 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
180 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
181 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700182
Colin Cross619b9ab2020-11-20 18:44:31 +0000183 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)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700189 }
Colin Cross619b9ab2020-11-20 18:44:31 +0000190 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)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700197 if err != nil {
198 return err
199 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700200 }
201
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700202 commandDescription := rawCommand
203
Jeff Gastonefc1b412017-03-29 17:29:06 -0700204 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()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700209
Jeff Gastonefc1b412017-03-29 17:29:06 -0700210 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Cross619b9ab2020-11-20 18:44:31 +0000211 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700212 } else if err != nil {
Colin Cross619b9ab2020-11-20 18:44:31 +0000213 return err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700214 }
215
Colin Cross619b9ab2020-11-20 18:44:31 +0000216 // 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 }
Jeff Gaston90cfb092017-09-26 16:46:10 -0700229 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 {
Colin Cross619b9ab2020-11-20 18:44:31 +0000239 errorMessage += " " + missingOutputError + "\n"
Jeff Gaston90cfb092017-09-26 16:46:10 -0700240 }
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
Colin Cross619b9ab2020-11-20 18:44:31 +0000256 // 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)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700260 }
261 // the created files match the declared files; now move them
Colin Cross619b9ab2020-11-20 18:44:31 +0000262 for _, filePath := range allOutputs {
263 tempPath := filepath.Join(tempDir, filePath)
264 destPath := filePath
265 if len(outputRoot) != 0 {
266 destPath = filepath.Join(outputRoot, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700267 }
Colin Cross619b9ab2020-11-20 18:44:31 +0000268 err := os.MkdirAll(filepath.Dir(destPath), 0777)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800269 if err != nil {
270 return err
271 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700272
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()
Colin Cross619b9ab2020-11-20 18:44:31 +0000276 err = os.Chtimes(tempPath, now, now)
277 if err != nil {
278 return err
279 }
280
281 err = os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700282 if err != nil {
283 return err
284 }
285 }
Colin Cross619b9ab2020-11-20 18:44:31 +0000286
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?
Jeff Gastonefc1b412017-03-29 17:29:06 -0700308 return nil
309}