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 "crypto/sha1" 20 "encoding/hex" 21 "errors" 22 "flag" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "strconv" 30 "strings" 31 "time" 32 33 "android/soong/cmd/sbox/sbox_proto" 34 "android/soong/makedeps" 35 "android/soong/response" 36 37 "github.com/golang/protobuf/proto" 38) 39 40var ( 41 sandboxesRoot string 42 manifestFile string 43 keepOutDir bool 44) 45 46const ( 47 depFilePlaceholder = "__SBOX_DEPFILE__" 48 sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__" 49) 50 51func init() { 52 flag.StringVar(&sandboxesRoot, "sandbox-path", "", 53 "root of temp directory to put the sandbox into") 54 flag.StringVar(&manifestFile, "manifest", "", 55 "textproto manifest describing the sandboxed command(s)") 56 flag.BoolVar(&keepOutDir, "keep-out-dir", false, 57 "whether to keep the sandbox directory when done") 58} 59 60func usageViolation(violation string) { 61 if violation != "" { 62 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation) 63 } 64 65 fmt.Fprintf(os.Stderr, 66 "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n") 67 68 flag.PrintDefaults() 69 70 os.Exit(1) 71} 72 73func main() { 74 flag.Usage = func() { 75 usageViolation("") 76 } 77 flag.Parse() 78 79 error := run() 80 if error != nil { 81 fmt.Fprintln(os.Stderr, error) 82 os.Exit(1) 83 } 84} 85 86func findAllFilesUnder(root string) (paths []string) { 87 paths = []string{} 88 filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 89 if !info.IsDir() { 90 relPath, err := filepath.Rel(root, path) 91 if err != nil { 92 // couldn't find relative path from ancestor? 93 panic(err) 94 } 95 paths = append(paths, relPath) 96 } 97 return nil 98 }) 99 return paths 100} 101 102func run() error { 103 if manifestFile == "" { 104 usageViolation("--manifest <manifest> is required and must be non-empty") 105 } 106 if sandboxesRoot == "" { 107 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR, 108 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so 109 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable 110 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it) 111 // and by passing it as a parameter we don't need to duplicate its value 112 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") 113 } 114 115 manifest, err := readManifest(manifestFile) 116 117 if len(manifest.Commands) == 0 { 118 return fmt.Errorf("at least one commands entry is required in %q", manifestFile) 119 } 120 121 // setup sandbox directory 122 err = os.MkdirAll(sandboxesRoot, 0777) 123 if err != nil { 124 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err) 125 } 126 127 // This tool assumes that there are no two concurrent runs with the same 128 // manifestFile. It should therefore be safe to use the hash of the 129 // manifestFile as the temporary directory name. We do this because it 130 // makes the temporary directory name deterministic. There are some 131 // tools that embed the name of the temporary output in the output, and 132 // they otherwise cause non-determinism, which then poisons actions 133 // depending on this one. 134 hash := sha1.New() 135 hash.Write([]byte(manifestFile)) 136 tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil))) 137 138 err = os.RemoveAll(tempDir) 139 if err != nil { 140 return err 141 } 142 err = os.MkdirAll(tempDir, 0777) 143 if err != nil { 144 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err) 145 } 146 147 // In the common case, the following line of code is what removes the sandbox 148 // If a fatal error occurs (such as if our Go process is killed unexpectedly), 149 // then at the beginning of the next build, Soong will wipe the temporary 150 // directory. 151 defer func() { 152 // in some cases we decline to remove the temp dir, to facilitate debugging 153 if !keepOutDir { 154 os.RemoveAll(tempDir) 155 } 156 }() 157 158 // If there is more than one command in the manifest use a separate directory for each one. 159 useSubDir := len(manifest.Commands) > 1 160 var commandDepFiles []string 161 162 for i, command := range manifest.Commands { 163 localTempDir := tempDir 164 if useSubDir { 165 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i)) 166 } 167 depFile, err := runCommand(command, localTempDir) 168 if err != nil { 169 // Running the command failed, keep the temporary output directory around in 170 // case a user wants to inspect it for debugging purposes. Soong will delete 171 // it at the beginning of the next build anyway. 172 keepOutDir = true 173 return err 174 } 175 if depFile != "" { 176 commandDepFiles = append(commandDepFiles, depFile) 177 } 178 } 179 180 outputDepFile := manifest.GetOutputDepfile() 181 if len(commandDepFiles) > 0 && outputDepFile == "" { 182 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file", 183 depFilePlaceholder) 184 } 185 186 if outputDepFile != "" { 187 // Merge the depfiles from each command in the manifest to a single output depfile. 188 err = rewriteDepFiles(commandDepFiles, outputDepFile) 189 if err != nil { 190 return fmt.Errorf("failed merging depfiles: %w", err) 191 } 192 } 193 194 return nil 195} 196 197// readManifest reads an sbox manifest from a textproto file. 198func readManifest(file string) (*sbox_proto.Manifest, error) { 199 manifestData, err := ioutil.ReadFile(file) 200 if err != nil { 201 return nil, fmt.Errorf("error reading manifest %q: %w", file, err) 202 } 203 204 manifest := sbox_proto.Manifest{} 205 206 err = proto.UnmarshalText(string(manifestData), &manifest) 207 if err != nil { 208 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err) 209 } 210 211 return &manifest, nil 212} 213 214// runCommand runs a single command from a manifest. If the command references the 215// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used. 216func runCommand(command *sbox_proto.Command, tempDir string) (depFile string, err error) { 217 rawCommand := command.GetCommand() 218 if rawCommand == "" { 219 return "", fmt.Errorf("command is required") 220 } 221 222 pathToTempDirInSbox := tempDir 223 if command.GetChdir() { 224 pathToTempDirInSbox = "." 225 } 226 227 err = os.MkdirAll(tempDir, 0777) 228 if err != nil { 229 return "", fmt.Errorf("failed to create %q: %w", tempDir, err) 230 } 231 232 // Copy in any files specified by the manifest. 233 err = copyFiles(command.CopyBefore, "", tempDir, false) 234 if err != nil { 235 return "", err 236 } 237 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox) 238 if err != nil { 239 return "", err 240 } 241 242 if strings.Contains(rawCommand, depFilePlaceholder) { 243 depFile = filepath.Join(pathToTempDirInSbox, "deps.d") 244 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1) 245 } 246 247 if strings.Contains(rawCommand, sandboxDirPlaceholder) { 248 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1) 249 } 250 251 // Emulate ninja's behavior of creating the directories for any output files before 252 // running the command. 253 err = makeOutputDirs(command.CopyAfter, tempDir) 254 if err != nil { 255 return "", err 256 } 257 258 cmd := exec.Command("bash", "-c", rawCommand) 259 buf := &bytes.Buffer{} 260 cmd.Stdin = os.Stdin 261 cmd.Stdout = buf 262 cmd.Stderr = buf 263 264 if command.GetChdir() { 265 cmd.Dir = tempDir 266 path := os.Getenv("PATH") 267 absPath, err := makeAbsPathEnv(path) 268 if err != nil { 269 return "", err 270 } 271 err = os.Setenv("PATH", absPath) 272 if err != nil { 273 return "", fmt.Errorf("Failed to update PATH: %w", err) 274 } 275 } 276 err = cmd.Run() 277 278 if err != nil { 279 // The command failed, do a best effort copy of output files out of the sandbox. This is 280 // especially useful for linters with baselines that print an error message on failure 281 // with a command to copy the output lint errors to the new baseline. Use a copy instead of 282 // a move to leave the sandbox intact for manual inspection 283 copyFiles(command.CopyAfter, tempDir, "", true) 284 } 285 286 // If the command was executed but failed with an error, print a debugging message before 287 // the command's output so it doesn't scroll the real error message off the screen. 288 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { 289 fmt.Fprintf(os.Stderr, 290 "The failing command was run inside an sbox sandbox in temporary directory\n"+ 291 "%s\n"+ 292 "The failing command line was:\n"+ 293 "%s\n", 294 tempDir, rawCommand) 295 } 296 297 // Write the command's combined stdout/stderr. 298 os.Stdout.Write(buf.Bytes()) 299 300 if err != nil { 301 return "", err 302 } 303 304 missingOutputErrors := validateOutputFiles(command.CopyAfter, tempDir) 305 306 if len(missingOutputErrors) > 0 { 307 // find all created files for making a more informative error message 308 createdFiles := findAllFilesUnder(tempDir) 309 310 // build error message 311 errorMessage := "mismatch between declared and actual outputs\n" 312 errorMessage += "in sbox command(" + rawCommand + ")\n\n" 313 errorMessage += "in sandbox " + tempDir + ",\n" 314 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors)) 315 for _, missingOutputError := range missingOutputErrors { 316 errorMessage += " " + missingOutputError.Error() + "\n" 317 } 318 if len(createdFiles) < 1 { 319 errorMessage += "created 0 files." 320 } else { 321 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles)) 322 creationMessages := createdFiles 323 maxNumCreationLines := 10 324 if len(creationMessages) > maxNumCreationLines { 325 creationMessages = creationMessages[:maxNumCreationLines] 326 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines)) 327 } 328 for _, creationMessage := range creationMessages { 329 errorMessage += " " + creationMessage + "\n" 330 } 331 } 332 333 return "", errors.New(errorMessage) 334 } 335 // the created files match the declared files; now move them 336 err = moveFiles(command.CopyAfter, tempDir, "") 337 338 return depFile, nil 339} 340 341// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied 342// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files 343// so that the tools don't have to. 344func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error { 345 for _, copyPair := range copies { 346 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom())) 347 err := os.MkdirAll(dir, 0777) 348 if err != nil { 349 return err 350 } 351 } 352 return nil 353} 354 355// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox 356// were created by the command. 357func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir string) []error { 358 var missingOutputErrors []error 359 for _, copyPair := range copies { 360 fromPath := joinPath(sandboxDir, copyPair.GetFrom()) 361 fileInfo, err := os.Stat(fromPath) 362 if err != nil { 363 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath)) 364 continue 365 } 366 if fileInfo.IsDir() { 367 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath)) 368 } 369 } 370 return missingOutputErrors 371} 372 373// copyFiles copies files in or out of the sandbox. If allowFromNotExists is true then errors 374// caused by a from path not existing are ignored. 375func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, allowFromNotExists bool) error { 376 for _, copyPair := range copies { 377 fromPath := joinPath(fromDir, copyPair.GetFrom()) 378 toPath := joinPath(toDir, copyPair.GetTo()) 379 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), allowFromNotExists) 380 if err != nil { 381 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err) 382 } 383 } 384 return nil 385} 386 387// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the 388// permissions. If allowFromNotExists is true it returns nil if the from path doesn't exist. 389func copyOneFile(from string, to string, forceExecutable, allowFromNotExists bool) error { 390 err := os.MkdirAll(filepath.Dir(to), 0777) 391 if err != nil { 392 return err 393 } 394 395 stat, err := os.Stat(from) 396 if err != nil { 397 if os.IsNotExist(err) && allowFromNotExists { 398 return nil 399 } 400 return err 401 } 402 403 perm := stat.Mode() 404 if forceExecutable { 405 perm = perm | 0100 // u+x 406 } 407 408 in, err := os.Open(from) 409 if err != nil { 410 return err 411 } 412 defer in.Close() 413 414 // Remove the target before copying. In most cases the file won't exist, but if there are 415 // duplicate copy rules for a file and the source file was read-only the second copy could 416 // fail. 417 err = os.Remove(to) 418 if err != nil && !os.IsNotExist(err) { 419 return err 420 } 421 422 out, err := os.Create(to) 423 if err != nil { 424 return err 425 } 426 defer func() { 427 out.Close() 428 if err != nil { 429 os.Remove(to) 430 } 431 }() 432 433 _, err = io.Copy(out, in) 434 if err != nil { 435 return err 436 } 437 438 if err = out.Close(); err != nil { 439 return err 440 } 441 442 if err = os.Chmod(to, perm); err != nil { 443 return err 444 } 445 446 return nil 447} 448 449// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files 450// listed into the sandbox. 451func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error { 452 for _, rspFile := range rspFiles { 453 err := copyOneRspFile(rspFile, toDir, toDirInSandbox) 454 if err != nil { 455 return err 456 } 457 } 458 return nil 459} 460 461// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files 462// listed into the sandbox. 463func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error { 464 in, err := os.Open(rspFile.GetFile()) 465 if err != nil { 466 return err 467 } 468 defer in.Close() 469 470 files, err := response.ReadRspFile(in) 471 if err != nil { 472 return err 473 } 474 475 for i, from := range files { 476 // Convert the real path of the input file into the path inside the sandbox using the 477 // path mappings. 478 to := applyPathMappings(rspFile.PathMappings, from) 479 480 // Copy the file into the sandbox. 481 err := copyOneFile(from, joinPath(toDir, to), false, false) 482 if err != nil { 483 return err 484 } 485 486 // Rewrite the name in the list of files to be relative to the sandbox directory. 487 files[i] = joinPath(toDirInSandbox, to) 488 } 489 490 // Convert the real path of the rsp file into the path inside the sandbox using the path 491 // mappings. 492 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile())) 493 494 err = os.MkdirAll(filepath.Dir(outRspFile), 0777) 495 if err != nil { 496 return err 497 } 498 499 out, err := os.Create(outRspFile) 500 if err != nil { 501 return err 502 } 503 defer out.Close() 504 505 // Write the rsp file with converted paths into the sandbox. 506 err = response.WriteRspFile(out, files) 507 if err != nil { 508 return err 509 } 510 511 return nil 512} 513 514// applyPathMappings takes a list of path mappings and a path, and returns the path with the first 515// matching path mapping applied. If the path does not match any of the path mappings then it is 516// returned unmodified. 517func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string { 518 for _, mapping := range pathMappings { 519 if strings.HasPrefix(path, mapping.GetFrom()+"/") { 520 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/")) 521 } 522 } 523 return path 524} 525 526// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted 527// to moving files where the source and destination are in the same filesystem. This is OK for 528// sbox because the temporary directory is inside the out directory. It updates the timestamp 529// of the new file. 530func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error { 531 for _, copyPair := range copies { 532 fromPath := joinPath(fromDir, copyPair.GetFrom()) 533 toPath := joinPath(toDir, copyPair.GetTo()) 534 err := os.MkdirAll(filepath.Dir(toPath), 0777) 535 if err != nil { 536 return err 537 } 538 539 err = os.Rename(fromPath, toPath) 540 if err != nil { 541 return err 542 } 543 544 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract 545 // files with old timestamps). 546 now := time.Now() 547 err = os.Chtimes(toPath, now, now) 548 if err != nil { 549 return err 550 } 551 } 552 return nil 553} 554 555// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory 556// to an output file. 557func rewriteDepFiles(ins []string, out string) error { 558 var mergedDeps []string 559 for _, in := range ins { 560 data, err := ioutil.ReadFile(in) 561 if err != nil { 562 return err 563 } 564 565 deps, err := makedeps.Parse(in, bytes.NewBuffer(data)) 566 if err != nil { 567 return err 568 } 569 mergedDeps = append(mergedDeps, deps.Inputs...) 570 } 571 572 deps := makedeps.Deps{ 573 // Ninja doesn't care what the output file is, so we can use any string here. 574 Output: "outputfile", 575 Inputs: mergedDeps, 576 } 577 578 // Make the directory for the output depfile in case it is in a different directory 579 // than any of the output files. 580 outDir := filepath.Dir(out) 581 err := os.MkdirAll(outDir, 0777) 582 if err != nil { 583 return fmt.Errorf("failed to create %q: %w", outDir, err) 584 } 585 586 return ioutil.WriteFile(out, deps.Print(), 0666) 587} 588 589// joinPath wraps filepath.Join but returns file without appending to dir if file is 590// absolute. 591func joinPath(dir, file string) string { 592 if filepath.IsAbs(file) { 593 return file 594 } 595 return filepath.Join(dir, file) 596} 597 598func makeAbsPathEnv(pathEnv string) (string, error) { 599 pathEnvElements := filepath.SplitList(pathEnv) 600 for i, p := range pathEnvElements { 601 if !filepath.IsAbs(p) { 602 absPath, err := filepath.Abs(p) 603 if err != nil { 604 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err) 605 } 606 pathEnvElements[i] = absPath 607 } 608 } 609 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil 610} 611