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"
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
Bill Peckhamc087be12020-02-13 15:55:10 -080039 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")
45 flag.StringVar(&rawCommand, "c", "",
46 "command to run")
47 flag.StringVar(&outputRoot, "output-root", "",
48 "root of directory to copy outputs into")
49 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
50 "whether to keep the sandbox directory when done")
51
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__")
Jeff Gaston8a88db52017-11-06 13:33:14 -080054
Bill Peckhamc087be12020-02-13 15:55:10 -080055 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,
Bill Peckhamc087be12020-02-13 15:55:10 -080065 "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 -070066 "\n"+
Jeff Gaston8a88db52017-11-06 13:33:14 -080067 "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 {
Jeff Gaston02a684b2017-10-27 14:59:27 -0700106 if rawCommand == "" {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700107 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 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700117 if len(outputRoot) == 0 {
Jeff Gaston93f0f372017-11-01 13:33:02 -0700118 usageViolation("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700119 }
120
Jeff Gaston93f0f372017-11-01 13:33:02 -0700121 // 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
Jeff Gaston8a88db52017-11-06 13:33:14 -0800130 // setup directories
131 err := os.MkdirAll(sandboxesRoot, 0777)
132 if err != nil {
133 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
142 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700143
144 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700145
Jeff Gaston02a684b2017-10-27 14:59:27 -0700146 for i, filePath := range outputsVarEntries {
Colin Crossbaccf5b2018-02-21 14:07:48 -0800147 if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") {
148 return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700149 }
Colin Crossbaccf5b2018-02-21 14:07:48 -0800150 outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700151 }
152
153 allOutputs = append([]string(nil), outputsVarEntries...)
154
Jeff Gaston93f0f372017-11-01 13:33:02 -0700155 if depfileOut != "" {
156 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700157 if err != nil {
158 return err
159 }
160 allOutputs = append(allOutputs, sandboxedDepfile)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700161 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 {
166 return fmt.Errorf("Failed to create temp dir: %s", err)
167 }
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),
171 // 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
179 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
180 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
181 }
182
183 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{}
Jeff Gaston02a684b2017-10-27 14:59:27 -0700186 for _, outputPath := range outputsVarEntries {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700187 tempOutPath := path.Join(tempDir, outputPath)
188 tempOutPaths = append(tempOutPaths, tempOutPath)
189 }
190 pathsText := strings.Join(tempOutPaths, " ")
191 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
192 }
193
Jeff Gaston02a684b2017-10-27 14:59:27 -0700194 for _, filePath := range allOutputs {
195 dir := path.Join(tempDir, filepath.Dir(filePath))
196 err = os.MkdirAll(dir, 0777)
197 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() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700211 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 {
213 return err
214 }
215
Jeff Gastonf49082a2017-06-07 13:22:22 -0700216 // validate that all files are created properly
Jeff Gaston90cfb092017-09-26 16:46:10 -0700217 var missingOutputErrors []string
Jeff Gaston02a684b2017-10-27 14:59:27 -0700218 for _, filePath := range allOutputs {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700219 tempPath := filepath.Join(tempDir, filePath)
220 fileInfo, err := os.Stat(tempPath)
221 if err != nil {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700222 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
Jeff Gastonf49082a2017-06-07 13:22:22 -0700223 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700224 }
225 if fileInfo.IsDir() {
Jeff Gaston90cfb092017-09-26 16:46:10 -0700226 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700227 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700228 }
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 {
239 errorMessage += " " + missingOutputError + "\n"
240 }
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
Jeff Gastonf49082a2017-06-07 13:22:22 -0700256 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
257 // Soong will delete it later anyway.
Jeff Gaston93f0f372017-11-01 13:33:02 -0700258 keepOutDir = true
Jeff Gaston90cfb092017-09-26 16:46:10 -0700259 return errors.New(errorMessage)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700260 }
261 // the created files match the declared files; now move them
Jeff Gaston02a684b2017-10-27 14:59:27 -0700262 for _, filePath := range allOutputs {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700263 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700264 destPath := filePath
265 if len(outputRoot) != 0 {
266 destPath = filepath.Join(outputRoot, filePath)
267 }
Jeff Gaston8a88db52017-11-06 13:33:14 -0800268 err := os.MkdirAll(filepath.Dir(destPath), 0777)
269 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()
276 err = os.Chtimes(tempPath, now, now)
277 if err != nil {
278 return err
279 }
280
Jeff Gaston8a88db52017-11-06 13:33:14 -0800281 err = os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700282 if err != nil {
283 return err
284 }
285 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700286
Dan Willemsenc89b6f12019-08-29 14:47:40 -0700287 // 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
Jeff Gastonefc1b412017-03-29 17:29:06 -0700307 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
308 return nil
309}