1// 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 ( 18 "bytes" 19 "errors" 20 "flag" 21 "fmt" 22 "io/ioutil" 23 "os" 24 "os/exec" 25 "path" 26 "path/filepath" 27 "strings" 28 "time" 29 30 "android/soong/makedeps" 31) 32 33var ( 34 sandboxesRoot string 35 rawCommand string 36 outputRoot string 37 keepOutDir bool 38 depfileOut string 39 inputHash string 40) 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__") 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.") 57} 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, 65 "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") 70 71 flag.PrintDefaults() 72 73 os.Exit(1) 74} 75 76func main() { 77 flag.Usage = func() { 78 usageViolation("") 79 } 80 flag.Parse() 81 82 error := run() 83 if error != nil { 84 fmt.Fprintln(os.Stderr, error) 85 os.Exit(1) 86 } 87} 88 89func 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 105func run() error { 106 if rawCommand == "" { 107 usageViolation("-c <commandToRun> is required and must be non-empty") 108 } 109 if sandboxesRoot == "" { 110 // 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 115 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") 116 } 117 if len(outputRoot) == 0 { 118 usageViolation("--output-root <outputRoot> is required and must be non-empty") 119 } 120 121 // 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) 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 } 143 144 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox") 145 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 165 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 172 defer func() { 173 // in some cases we decline to remove the temp dir, to facilitate debugging 174 if !keepOutDir { 175 os.RemoveAll(tempDir) 176 } 177 }() 178 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{} 186 for _, outputPath := range outputsVarEntries { 187 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 194 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 } 200 } 201 202 commandDescription := rawCommand 203 204 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() 209 210 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { 211 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error()) 212 } else if err != nil { 213 return err 214 } 215 216 // 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 } 229 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 256 // 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) 260 } 261 // the created files match the declared files; now move them 262 for _, filePath := range allOutputs { 263 tempPath := filepath.Join(tempDir, filePath) 264 destPath := filePath 265 if len(outputRoot) != 0 { 266 destPath = filepath.Join(outputRoot, filePath) 267 } 268 err := os.MkdirAll(filepath.Dir(destPath), 0777) 269 if err != nil { 270 return err 271 } 272 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 281 err = os.Rename(tempPath, destPath) 282 if err != nil { 283 return err 284 } 285 } 286 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? 308 return nil 309} 310