github.com/westcoastroms/westcoastroms-build@v0.0.0-20190928114312-2350e5a73030/build/soong/cmd/sbox/sbox.go (about) 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 15 package main 16 17 import ( 18 "errors" 19 "flag" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "os/exec" 24 "path" 25 "path/filepath" 26 "strings" 27 ) 28 29 var ( 30 sandboxesRoot string 31 rawCommand string 32 outputRoot string 33 keepOutDir bool 34 depfileOut string 35 ) 36 37 func init() { 38 flag.StringVar(&sandboxesRoot, "sandbox-path", "", 39 "root of temp directory to put the sandbox into") 40 flag.StringVar(&rawCommand, "c", "", 41 "command to run") 42 flag.StringVar(&outputRoot, "output-root", "", 43 "root of directory to copy outputs into") 44 flag.BoolVar(&keepOutDir, "keep-out-dir", false, 45 "whether to keep the sandbox directory when done") 46 47 flag.StringVar(&depfileOut, "depfile-out", "", 48 "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__") 49 50 } 51 52 func usageViolation(violation string) { 53 if violation != "" { 54 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation) 55 } 56 57 fmt.Fprintf(os.Stderr, 58 "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> --overwrite [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+ 59 "\n"+ 60 "Deletes <outputRoot>,"+ 61 "runs <commandToRun>,"+ 62 "and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n") 63 64 flag.PrintDefaults() 65 66 os.Exit(1) 67 } 68 69 func main() { 70 flag.Usage = func() { 71 usageViolation("") 72 } 73 flag.Parse() 74 75 error := run() 76 if error != nil { 77 fmt.Fprintln(os.Stderr, error) 78 os.Exit(1) 79 } 80 } 81 82 func findAllFilesUnder(root string) (paths []string) { 83 paths = []string{} 84 filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 85 if !info.IsDir() { 86 relPath, err := filepath.Rel(root, path) 87 if err != nil { 88 // couldn't find relative path from ancestor? 89 panic(err) 90 } 91 paths = append(paths, relPath) 92 } 93 return nil 94 }) 95 return paths 96 } 97 98 func run() error { 99 if rawCommand == "" { 100 usageViolation("-c <commandToRun> is required and must be non-empty") 101 } 102 if sandboxesRoot == "" { 103 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR, 104 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so 105 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable 106 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it) 107 // and by passing it as a parameter we don't need to duplicate its value 108 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") 109 } 110 if len(outputRoot) == 0 { 111 usageViolation("--output-root <outputRoot> is required and must be non-empty") 112 } 113 114 // the contents of the __SBOX_OUT_FILES__ variable 115 outputsVarEntries := flag.Args() 116 if len(outputsVarEntries) == 0 { 117 usageViolation("at least one output file must be given") 118 } 119 120 // all outputs 121 var allOutputs []string 122 123 // setup directories 124 err := os.MkdirAll(sandboxesRoot, 0777) 125 if err != nil { 126 return err 127 } 128 err = os.RemoveAll(outputRoot) 129 if err != nil { 130 return err 131 } 132 err = os.MkdirAll(outputRoot, 0777) 133 if err != nil { 134 return err 135 } 136 137 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox") 138 139 for i, filePath := range outputsVarEntries { 140 if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") { 141 return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`") 142 } 143 outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/") 144 } 145 146 allOutputs = append([]string(nil), outputsVarEntries...) 147 148 if depfileOut != "" { 149 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut) 150 if err != nil { 151 return err 152 } 153 allOutputs = append(allOutputs, sandboxedDepfile) 154 if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") { 155 return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__") 156 } 157 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1) 158 159 } 160 161 if err != nil { 162 return fmt.Errorf("Failed to create temp dir: %s", err) 163 } 164 165 // In the common case, the following line of code is what removes the sandbox 166 // If a fatal error occurs (such as if our Go process is killed unexpectedly), 167 // then at the beginning of the next build, Soong will retry the cleanup 168 defer func() { 169 // in some cases we decline to remove the temp dir, to facilitate debugging 170 if !keepOutDir { 171 os.RemoveAll(tempDir) 172 } 173 }() 174 175 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") { 176 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1) 177 } 178 179 if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") { 180 // expands into a space-separated list of output files to be generated into the sandbox directory 181 tempOutPaths := []string{} 182 for _, outputPath := range outputsVarEntries { 183 tempOutPath := path.Join(tempDir, outputPath) 184 tempOutPaths = append(tempOutPaths, tempOutPath) 185 } 186 pathsText := strings.Join(tempOutPaths, " ") 187 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1) 188 } 189 190 for _, filePath := range allOutputs { 191 dir := path.Join(tempDir, filepath.Dir(filePath)) 192 err = os.MkdirAll(dir, 0777) 193 if err != nil { 194 return err 195 } 196 } 197 198 commandDescription := rawCommand 199 200 cmd := exec.Command("bash", "-c", rawCommand) 201 cmd.Stdin = os.Stdin 202 cmd.Stdout = os.Stdout 203 cmd.Stderr = os.Stderr 204 err = cmd.Run() 205 206 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { 207 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error()) 208 } else if err != nil { 209 return err 210 } 211 212 // validate that all files are created properly 213 var missingOutputErrors []string 214 for _, filePath := range allOutputs { 215 tempPath := filepath.Join(tempDir, filePath) 216 fileInfo, err := os.Stat(tempPath) 217 if err != nil { 218 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath)) 219 continue 220 } 221 if fileInfo.IsDir() { 222 missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath)) 223 } 224 } 225 if len(missingOutputErrors) > 0 { 226 // find all created files for making a more informative error message 227 createdFiles := findAllFilesUnder(tempDir) 228 229 // build error message 230 errorMessage := "mismatch between declared and actual outputs\n" 231 errorMessage += "in sbox command(" + commandDescription + ")\n\n" 232 errorMessage += "in sandbox " + tempDir + ",\n" 233 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors)) 234 for _, missingOutputError := range missingOutputErrors { 235 errorMessage += " " + missingOutputError + "\n" 236 } 237 if len(createdFiles) < 1 { 238 errorMessage += "created 0 files." 239 } else { 240 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles)) 241 creationMessages := createdFiles 242 maxNumCreationLines := 10 243 if len(creationMessages) > maxNumCreationLines { 244 creationMessages = creationMessages[:maxNumCreationLines] 245 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines)) 246 } 247 for _, creationMessage := range creationMessages { 248 errorMessage += " " + creationMessage + "\n" 249 } 250 } 251 252 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes. 253 // Soong will delete it later anyway. 254 keepOutDir = true 255 return errors.New(errorMessage) 256 } 257 // the created files match the declared files; now move them 258 for _, filePath := range allOutputs { 259 tempPath := filepath.Join(tempDir, filePath) 260 destPath := filePath 261 if len(outputRoot) != 0 { 262 destPath = filepath.Join(outputRoot, filePath) 263 } 264 err := os.MkdirAll(filepath.Dir(destPath), 0777) 265 if err != nil { 266 return err 267 } 268 err = os.Rename(tempPath, destPath) 269 if err != nil { 270 return err 271 } 272 } 273 274 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning? 275 return nil 276 }