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 "errors" 19 "flag" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "os/exec" 24 "path" 25 "path/filepath" 26 "strings" 27 "time" 28) 29 30var ( 31 sandboxesRoot string 32 rawCommand string 33 outputRoot string 34 keepOutDir bool 35 depfileOut string 36) 37 38func init() { 39 flag.StringVar(&sandboxesRoot, "sandbox-path", "", 40 "root of temp directory to put the sandbox into") 41 flag.StringVar(&rawCommand, "c", "", 42 "command to run") 43 flag.StringVar(&outputRoot, "output-root", "", 44 "root of directory to copy outputs into") 45 flag.BoolVar(&keepOutDir, "keep-out-dir", false, 46 "whether to keep the sandbox directory when done") 47 48 flag.StringVar(&depfileOut, "depfile-out", "", 49 "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__") 50 51} 52 53func usageViolation(violation string) { 54 if violation != "" { 55 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation) 56 } 57 58 fmt.Fprintf(os.Stderr, 59 "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> --overwrite [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+ 60 "\n"+ 61 "Deletes <outputRoot>,"+ 62 "runs <commandToRun>,"+ 63 "and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n") 64 65 flag.PrintDefaults() 66 67 os.Exit(1) 68} 69 70func main() { 71 flag.Usage = func() { 72 usageViolation("") 73 } 74 flag.Parse() 75 76 error := run() 77 if error != nil { 78 fmt.Fprintln(os.Stderr, error) 79 os.Exit(1) 80 } 81} 82 83func findAllFilesUnder(root string) (paths []string) { 84 paths = []string{} 85 filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 86 if !info.IsDir() { 87 relPath, err := filepath.Rel(root, path) 88 if err != nil { 89 // couldn't find relative path from ancestor? 90 panic(err) 91 } 92 paths = append(paths, relPath) 93 } 94 return nil 95 }) 96 return paths 97} 98 99func run() error { 100 if rawCommand == "" { 101 usageViolation("-c <commandToRun> is required and must be non-empty") 102 } 103 if sandboxesRoot == "" { 104 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR, 105 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so 106 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable 107 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it) 108 // and by passing it as a parameter we don't need to duplicate its value 109 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") 110 } 111 if len(outputRoot) == 0 { 112 usageViolation("--output-root <outputRoot> is required and must be non-empty") 113 } 114 115 // the contents of the __SBOX_OUT_FILES__ variable 116 outputsVarEntries := flag.Args() 117 if len(outputsVarEntries) == 0 { 118 usageViolation("at least one output file must be given") 119 } 120 121 // all outputs 122 var allOutputs []string 123 124 // setup directories 125 err := os.MkdirAll(sandboxesRoot, 0777) 126 if err != nil { 127 return err 128 } 129 err = os.RemoveAll(outputRoot) 130 if err != nil { 131 return err 132 } 133 err = os.MkdirAll(outputRoot, 0777) 134 if err != nil { 135 return err 136 } 137 138 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox") 139 140 for i, filePath := range outputsVarEntries { 141 if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") { 142 return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`") 143 } 144 outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/") 145 } 146 147 allOutputs = append([]string(nil), outputsVarEntries...) 148 149 if depfileOut != "" { 150 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut) 151 if err != nil { 152 return err 153 } 154 allOutputs = append(allOutputs, sandboxedDepfile) 155 if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") { 156 return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__") 157 } 158 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1) 159 160 } 161 162 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 169 defer func() { 170 // in some cases we decline to remove the temp dir, to facilitate debugging 171 if !keepOutDir { 172 os.RemoveAll(tempDir) 173 } 174 }() 175 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{} 183 for _, outputPath := range outputsVarEntries { 184 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 191 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 } 197 } 198 199 commandDescription := rawCommand 200 201 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() 206 207 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { 208 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error()) 209 } else if err != nil { 210 return err 211 } 212 213 // validate that all files are created properly 214 var missingOutputErrors []string 215 for _, filePath := range allOutputs { 216 tempPath := filepath.Join(tempDir, filePath) 217 fileInfo, err := os.Stat(tempPath) 218 if err != nil { 219 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath)) 220 continue 221 } 222 if fileInfo.IsDir() { 223 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath)) 224 } 225 } 226 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 253 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes. 254 // Soong will delete it later anyway. 255 keepOutDir = true 256 return errors.New(errorMessage) 257 } 258 // the created files match the declared files; now move them 259 for _, filePath := range allOutputs { 260 tempPath := filepath.Join(tempDir, filePath) 261 destPath := filePath 262 if len(outputRoot) != 0 { 263 destPath = filepath.Join(outputRoot, filePath) 264 } 265 err := os.MkdirAll(filepath.Dir(destPath), 0777) 266 if err != nil { 267 return err 268 } 269 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 278 err = os.Rename(tempPath, destPath) 279 if err != nil { 280 return err 281 } 282 } 283 284 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning? 285 return nil 286} 287