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/fs" 26 "io/ioutil" 27 "os" 28 "os/exec" 29 "path/filepath" 30 "regexp" 31 "strconv" 32 "strings" 33 "time" 34 35 "android/soong/cmd/sbox/sbox_proto" 36 "android/soong/makedeps" 37 "android/soong/response" 38 39 "google.golang.org/protobuf/encoding/prototext" 40) 41 42var ( 43 sandboxesRoot string 44 outputDir string 45 manifestFile string 46 keepOutDir bool 47 writeIfChanged bool 48) 49 50const ( 51 depFilePlaceholder = "__SBOX_DEPFILE__" 52 sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__" 53) 54 55var envVarNameRegex = regexp.MustCompile("^[a-zA-Z0-9_-]+$") 56 57func init() { 58 flag.StringVar(&sandboxesRoot, "sandbox-path", "", 59 "root of temp directory to put the sandbox into") 60 flag.StringVar(&outputDir, "output-dir", "", 61 "directory which will contain all output files and only output files") 62 flag.StringVar(&manifestFile, "manifest", "", 63 "textproto manifest describing the sandboxed command(s)") 64 flag.BoolVar(&keepOutDir, "keep-out-dir", false, 65 "whether to keep the sandbox directory when done") 66 flag.BoolVar(&writeIfChanged, "write-if-changed", false, 67 "only write the output files if they have changed") 68} 69 70func usageViolation(violation string) { 71 if violation != "" { 72 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation) 73 } 74 75 fmt.Fprintf(os.Stderr, 76 "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n") 77 78 flag.PrintDefaults() 79 80 os.Exit(1) 81} 82 83func main() { 84 flag.Usage = func() { 85 usageViolation("") 86 } 87 flag.Parse() 88 89 error := run() 90 if error != nil { 91 fmt.Fprintln(os.Stderr, error) 92 os.Exit(1) 93 } 94} 95 96func findAllFilesUnder(root string) (paths []string) { 97 paths = []string{} 98 filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 99 if !info.IsDir() { 100 relPath, err := filepath.Rel(root, path) 101 if err != nil { 102 // couldn't find relative path from ancestor? 103 panic(err) 104 } 105 paths = append(paths, relPath) 106 } 107 return nil 108 }) 109 return paths 110} 111 112func run() error { 113 if manifestFile == "" { 114 usageViolation("--manifest <manifest> is required and must be non-empty") 115 } 116 if sandboxesRoot == "" { 117 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR, 118 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so 119 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable 120 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it) 121 // and by passing it as a parameter we don't need to duplicate its value 122 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") 123 } 124 125 manifest, err := readManifest(manifestFile) 126 if err != nil { 127 return err 128 } 129 130 if len(manifest.Commands) == 0 { 131 return fmt.Errorf("at least one commands entry is required in %q", manifestFile) 132 } 133 134 // setup sandbox directory 135 err = os.MkdirAll(sandboxesRoot, 0777) 136 if err != nil { 137 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err) 138 } 139 140 // This tool assumes that there are no two concurrent runs with the same 141 // manifestFile. It should therefore be safe to use the hash of the 142 // manifestFile as the temporary directory name. We do this because it 143 // makes the temporary directory name deterministic. There are some 144 // tools that embed the name of the temporary output in the output, and 145 // they otherwise cause non-determinism, which then poisons actions 146 // depending on this one. 147 hash := sha1.New() 148 hash.Write([]byte(manifestFile)) 149 tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil))) 150 151 err = os.RemoveAll(tempDir) 152 if err != nil { 153 return err 154 } 155 err = os.MkdirAll(tempDir, 0777) 156 if err != nil { 157 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err) 158 } 159 160 // In the common case, the following line of code is what removes the sandbox 161 // If a fatal error occurs (such as if our Go process is killed unexpectedly), 162 // then at the beginning of the next build, Soong will wipe the temporary 163 // directory. 164 defer func() { 165 // in some cases we decline to remove the temp dir, to facilitate debugging 166 if !keepOutDir { 167 os.RemoveAll(tempDir) 168 } 169 }() 170 171 // If there is more than one command in the manifest use a separate directory for each one. 172 useSubDir := len(manifest.Commands) > 1 173 var commandDepFiles []string 174 175 for i, command := range manifest.Commands { 176 localTempDir := tempDir 177 if useSubDir { 178 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i)) 179 } 180 depFile, err := runCommand(command, localTempDir, i) 181 if err != nil { 182 // Running the command failed, keep the temporary output directory around in 183 // case a user wants to inspect it for debugging purposes. Soong will delete 184 // it at the beginning of the next build anyway. 185 keepOutDir = true 186 return err 187 } 188 if depFile != "" { 189 commandDepFiles = append(commandDepFiles, depFile) 190 } 191 } 192 193 outputDepFile := manifest.GetOutputDepfile() 194 if len(commandDepFiles) > 0 && outputDepFile == "" { 195 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file", 196 depFilePlaceholder) 197 } 198 199 if outputDepFile != "" { 200 // Merge the depfiles from each command in the manifest to a single output depfile. 201 err = rewriteDepFiles(commandDepFiles, outputDepFile) 202 if err != nil { 203 return fmt.Errorf("failed merging depfiles: %w", err) 204 } 205 } 206 207 return nil 208} 209 210// createCommandScript will create and return an exec.Cmd that runs rawCommand. 211// 212// rawCommand is executed via a script in the sandbox. 213// scriptPath is the temporary where the script is created. 214// scriptPathInSandbox is the path to the script in the sbox environment. 215// 216// returns an exec.Cmd that can be ran from within sbox context if no error, or nil if error. 217// caller must ensure script is cleaned up if function succeeds. 218func createCommandScript(rawCommand, scriptPath, scriptPathInSandbox string) (*exec.Cmd, error) { 219 err := os.WriteFile(scriptPath, []byte(rawCommand), 0644) 220 if err != nil { 221 return nil, fmt.Errorf("failed to write command %s... to %s", 222 rawCommand[0:40], scriptPath) 223 } 224 return exec.Command("bash", scriptPathInSandbox), nil 225} 226 227// readManifest reads an sbox manifest from a textproto file. 228func readManifest(file string) (*sbox_proto.Manifest, error) { 229 manifestData, err := ioutil.ReadFile(file) 230 if err != nil { 231 return nil, fmt.Errorf("error reading manifest %q: %w", file, err) 232 } 233 234 manifest := sbox_proto.Manifest{} 235 236 err = prototext.Unmarshal(manifestData, &manifest) 237 if err != nil { 238 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err) 239 } 240 241 return &manifest, nil 242} 243 244func createEnv(command *sbox_proto.Command) ([]string, error) { 245 env := []string{} 246 if command.DontInheritEnv == nil || !*command.DontInheritEnv { 247 env = os.Environ() 248 } 249 for _, envVar := range command.Env { 250 if envVar.Name == nil || !envVarNameRegex.MatchString(*envVar.Name) { 251 name := "nil" 252 if envVar.Name != nil { 253 name = *envVar.Name 254 } 255 return nil, fmt.Errorf("Invalid environment variable name: %q", name) 256 } 257 if envVar.State == nil { 258 return nil, fmt.Errorf("Must set state") 259 } 260 switch state := envVar.State.(type) { 261 case *sbox_proto.EnvironmentVariable_Value: 262 env = append(env, *envVar.Name+"="+state.Value) 263 case *sbox_proto.EnvironmentVariable_Unset: 264 if !state.Unset { 265 return nil, fmt.Errorf("Can't have unset set to false") 266 } 267 prefix := *envVar.Name + "=" 268 for i := 0; i < len(env); i++ { 269 if strings.HasPrefix(env[i], prefix) { 270 env = append(env[:i], env[i+1:]...) 271 i-- 272 } 273 } 274 case *sbox_proto.EnvironmentVariable_Inherit: 275 if !state.Inherit { 276 return nil, fmt.Errorf("Can't have inherit set to false") 277 } 278 val, ok := os.LookupEnv(*envVar.Name) 279 if ok { 280 env = append(env, *envVar.Name+"="+val) 281 } 282 default: 283 return nil, fmt.Errorf("Unhandled state type") 284 } 285 } 286 return env, nil 287} 288 289// runCommand runs a single command from a manifest. If the command references the 290// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used. 291func runCommand(command *sbox_proto.Command, tempDir string, commandIndex int) (depFile string, err error) { 292 rawCommand := command.GetCommand() 293 if rawCommand == "" { 294 return "", fmt.Errorf("command is required") 295 } 296 297 // Remove files from the output directory 298 err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged)) 299 if err != nil { 300 return "", err 301 } 302 303 pathToTempDirInSbox := tempDir 304 if command.GetChdir() { 305 pathToTempDirInSbox = "." 306 } 307 308 err = os.MkdirAll(tempDir, 0777) 309 if err != nil { 310 return "", fmt.Errorf("failed to create %q: %w", tempDir, err) 311 } 312 313 // Copy in any files specified by the manifest. 314 err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite) 315 if err != nil { 316 return "", err 317 } 318 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox) 319 if err != nil { 320 return "", err 321 } 322 323 if strings.Contains(rawCommand, depFilePlaceholder) { 324 depFile = filepath.Join(pathToTempDirInSbox, "deps.d") 325 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1) 326 } 327 328 if strings.Contains(rawCommand, sandboxDirPlaceholder) { 329 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1) 330 } 331 332 // Emulate ninja's behavior of creating the directories for any output files before 333 // running the command. 334 err = makeOutputDirs(command.CopyAfter, tempDir) 335 if err != nil { 336 return "", err 337 } 338 339 scriptName := fmt.Sprintf("sbox_command.%d.bash", commandIndex) 340 scriptPath := joinPath(tempDir, scriptName) 341 scriptPathInSandbox := joinPath(pathToTempDirInSbox, scriptName) 342 cmd, err := createCommandScript(rawCommand, scriptPath, scriptPathInSandbox) 343 if err != nil { 344 return "", err 345 } 346 347 buf := &bytes.Buffer{} 348 cmd.Stdin = os.Stdin 349 cmd.Stdout = buf 350 cmd.Stderr = buf 351 352 if command.GetChdir() { 353 cmd.Dir = tempDir 354 path := os.Getenv("PATH") 355 absPath, err := makeAbsPathEnv(path) 356 if err != nil { 357 return "", err 358 } 359 err = os.Setenv("PATH", absPath) 360 if err != nil { 361 return "", fmt.Errorf("Failed to update PATH: %w", err) 362 } 363 } 364 365 cmd.Env, err = createEnv(command) 366 if err != nil { 367 return "", err 368 } 369 370 err = cmd.Run() 371 372 if err != nil { 373 // The command failed, do a best effort copy of output files out of the sandbox. This is 374 // especially useful for linters with baselines that print an error message on failure 375 // with a command to copy the output lint errors to the new baseline. Use a copy instead of 376 // a move to leave the sandbox intact for manual inspection 377 copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged)) 378 } 379 380 // If the command was executed but failed with an error, print a debugging message before 381 // the command's output so it doesn't scroll the real error message off the screen. 382 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { 383 fmt.Fprintf(os.Stderr, 384 "The failing command was run inside an sbox sandbox in temporary directory\n"+ 385 "%s\n"+ 386 "The failing command line can be found in\n"+ 387 "%s\n", 388 tempDir, scriptPath) 389 } 390 391 // Write the command's combined stdout/stderr. 392 os.Stdout.Write(buf.Bytes()) 393 394 if err != nil { 395 return "", err 396 } 397 398 err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand) 399 if err != nil { 400 return "", err 401 } 402 403 // the created files match the declared files; now move them 404 err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged)) 405 if err != nil { 406 return "", err 407 } 408 409 return depFile, nil 410} 411 412// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied 413// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files 414// so that the tools don't have to. 415func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error { 416 for _, copyPair := range copies { 417 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom())) 418 err := os.MkdirAll(dir, 0777) 419 if err != nil { 420 return err 421 } 422 } 423 return nil 424} 425 426// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox 427// were created by the command. 428func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error { 429 var missingOutputErrors []error 430 var incorrectOutputDirectoryErrors []error 431 for _, copyPair := range copies { 432 fromPath := joinPath(sandboxDir, copyPair.GetFrom()) 433 fileInfo, err := os.Stat(fromPath) 434 if err != nil { 435 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath)) 436 continue 437 } 438 if fileInfo.IsDir() { 439 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath)) 440 } 441 442 toPath := copyPair.GetTo() 443 if rel, err := filepath.Rel(outputDir, toPath); err != nil { 444 return err 445 } else if strings.HasPrefix(rel, "../") { 446 incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors, 447 fmt.Errorf("%s is not under %s", toPath, outputDir)) 448 } 449 } 450 451 const maxErrors = 25 452 453 if len(incorrectOutputDirectoryErrors) > 0 { 454 errorMessage := "" 455 more := 0 456 if len(incorrectOutputDirectoryErrors) > maxErrors { 457 more = len(incorrectOutputDirectoryErrors) - maxErrors 458 incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors] 459 } 460 461 for _, err := range incorrectOutputDirectoryErrors { 462 errorMessage += err.Error() + "\n" 463 } 464 if more > 0 { 465 errorMessage += fmt.Sprintf("...%v more", more) 466 } 467 468 return errors.New(errorMessage) 469 } 470 471 if len(missingOutputErrors) > 0 { 472 // find all created files for making a more informative error message 473 createdFiles := findAllFilesUnder(sandboxDir) 474 475 // build error message 476 errorMessage := "mismatch between declared and actual outputs\n" 477 errorMessage += "in sbox command(" + rawCommand + ")\n\n" 478 errorMessage += "in sandbox " + sandboxDir + ",\n" 479 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors)) 480 for _, missingOutputError := range missingOutputErrors { 481 errorMessage += " " + missingOutputError.Error() + "\n" 482 } 483 if len(createdFiles) < 1 { 484 errorMessage += "created 0 files." 485 } else { 486 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles)) 487 creationMessages := createdFiles 488 if len(creationMessages) > maxErrors { 489 creationMessages = creationMessages[:maxErrors] 490 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors)) 491 } 492 for _, creationMessage := range creationMessages { 493 errorMessage += " " + creationMessage + "\n" 494 } 495 } 496 497 return errors.New(errorMessage) 498 } 499 500 return nil 501} 502 503type existsType bool 504 505const ( 506 requireFromExists existsType = false 507 allowFromNotExists = true 508) 509 510type writeType bool 511 512const ( 513 alwaysWrite writeType = false 514 onlyWriteIfChanged = true 515) 516 517// copyFiles copies files in or out of the sandbox. If exists is allowFromNotExists then errors 518// caused by a from path not existing are ignored. If write is onlyWriteIfChanged then the output 519// file is compared to the input file and not written to if it is the same, avoiding updating 520// the timestamp. 521func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error { 522 for _, copyPair := range copies { 523 fromPath := joinPath(fromDir, copyPair.GetFrom()) 524 toPath := joinPath(toDir, copyPair.GetTo()) 525 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write) 526 if err != nil { 527 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err) 528 } 529 } 530 return nil 531} 532 533// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the 534// permissions. If exists is allowFromNotExists it returns nil if the from path doesn't exist. 535// If write is onlyWriteIfChanged then the output file is compared to the input file and not written to 536// if it is the same, avoiding updating the timestamp. If from is a symlink, the symlink itself 537// will be copied, instead of what it points to. 538func copyOneFile(from string, to string, forceExecutable bool, exists existsType, 539 write writeType) error { 540 err := os.MkdirAll(filepath.Dir(to), 0777) 541 if err != nil { 542 return err 543 } 544 545 stat, err := os.Lstat(from) 546 if err != nil { 547 if os.IsNotExist(err) && exists == allowFromNotExists { 548 return nil 549 } 550 return err 551 } 552 553 if stat.Mode()&fs.ModeSymlink != 0 { 554 linkTarget, err := os.Readlink(from) 555 if err != nil { 556 return err 557 } 558 if write == onlyWriteIfChanged { 559 toLinkTarget, err := os.Readlink(to) 560 if err == nil && toLinkTarget == linkTarget { 561 return nil 562 } 563 } 564 err = os.Remove(to) 565 if err != nil && !os.IsNotExist(err) { 566 return err 567 } 568 569 return os.Symlink(linkTarget, to) 570 } 571 572 perm := stat.Mode() 573 if forceExecutable { 574 perm = perm | 0100 // u+x 575 } 576 577 if write == onlyWriteIfChanged && filesHaveSameContents(from, to) { 578 return nil 579 } 580 581 in, err := os.Open(from) 582 if err != nil { 583 return err 584 } 585 defer in.Close() 586 587 // Remove the target before copying. In most cases the file won't exist, but if there are 588 // duplicate copy rules for a file and the source file was read-only the second copy could 589 // fail. 590 err = os.Remove(to) 591 if err != nil && !os.IsNotExist(err) { 592 return err 593 } 594 595 out, err := os.Create(to) 596 if err != nil { 597 return err 598 } 599 defer func() { 600 out.Close() 601 if err != nil { 602 os.Remove(to) 603 } 604 }() 605 606 _, err = io.Copy(out, in) 607 if err != nil { 608 return err 609 } 610 611 if err = out.Close(); err != nil { 612 return err 613 } 614 615 if err = os.Chmod(to, perm); err != nil { 616 return err 617 } 618 619 return nil 620} 621 622// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files 623// listed into the sandbox. 624func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error { 625 for _, rspFile := range rspFiles { 626 err := copyOneRspFile(rspFile, toDir, toDirInSandbox) 627 if err != nil { 628 return err 629 } 630 } 631 return nil 632} 633 634// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files 635// listed into the sandbox. 636func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error { 637 in, err := os.Open(rspFile.GetFile()) 638 if err != nil { 639 return err 640 } 641 defer in.Close() 642 643 files, err := response.ReadRspFile(in) 644 if err != nil { 645 return err 646 } 647 648 for i, from := range files { 649 // Convert the real path of the input file into the path inside the sandbox using the 650 // path mappings. 651 to := applyPathMappings(rspFile.PathMappings, from) 652 653 // Copy the file into the sandbox. 654 err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite) 655 if err != nil { 656 return err 657 } 658 659 // Rewrite the name in the list of files to be relative to the sandbox directory. 660 files[i] = joinPath(toDirInSandbox, to) 661 } 662 663 // Convert the real path of the rsp file into the path inside the sandbox using the path 664 // mappings. 665 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile())) 666 667 err = os.MkdirAll(filepath.Dir(outRspFile), 0777) 668 if err != nil { 669 return err 670 } 671 672 out, err := os.Create(outRspFile) 673 if err != nil { 674 return err 675 } 676 defer out.Close() 677 678 // Write the rsp file with converted paths into the sandbox. 679 err = response.WriteRspFile(out, files) 680 if err != nil { 681 return err 682 } 683 684 return nil 685} 686 687// applyPathMappings takes a list of path mappings and a path, and returns the path with the first 688// matching path mapping applied. If the path does not match any of the path mappings then it is 689// returned unmodified. 690func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string { 691 for _, mapping := range pathMappings { 692 if strings.HasPrefix(path, mapping.GetFrom()+"/") { 693 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/")) 694 } 695 } 696 return path 697} 698 699// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted 700// to moving files where the source and destination are in the same filesystem. This is OK for 701// sbox because the temporary directory is inside the out directory. If write is onlyWriteIfChanged 702// then the output file is compared to the input file and not written to if it is the same, avoiding 703// updating the timestamp. Otherwise it always updates the timestamp of the new file. 704func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error { 705 for _, copyPair := range copies { 706 fromPath := joinPath(fromDir, copyPair.GetFrom()) 707 toPath := joinPath(toDir, copyPair.GetTo()) 708 err := os.MkdirAll(filepath.Dir(toPath), 0777) 709 if err != nil { 710 return err 711 } 712 713 if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) { 714 continue 715 } 716 717 err = os.Rename(fromPath, toPath) 718 if err != nil { 719 return err 720 } 721 722 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract 723 // files with old timestamps). 724 now := time.Now() 725 err = os.Chtimes(toPath, now, now) 726 if err != nil { 727 return err 728 } 729 } 730 return nil 731} 732 733// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or 734// any files not listed in copies if write is onlyWriteIfChanged 735func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error { 736 if outputDir == "" { 737 return fmt.Errorf("output directory must be set") 738 } 739 740 if write == alwaysWrite { 741 // When writing all the output files remove the whole output directory 742 return os.RemoveAll(outputDir) 743 } 744 745 outputFiles := make(map[string]bool, len(copies)) 746 for _, copyPair := range copies { 747 outputFiles[copyPair.GetTo()] = true 748 } 749 750 existingFiles := findAllFilesUnder(outputDir) 751 for _, existingFile := range existingFiles { 752 fullExistingFile := filepath.Join(outputDir, existingFile) 753 if !outputFiles[fullExistingFile] { 754 err := os.Remove(fullExistingFile) 755 if err != nil { 756 return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err) 757 } 758 } 759 } 760 761 return nil 762} 763 764// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory 765// to an output file. 766func rewriteDepFiles(ins []string, out string) error { 767 var mergedDeps []string 768 for _, in := range ins { 769 data, err := ioutil.ReadFile(in) 770 if err != nil { 771 return err 772 } 773 774 deps, err := makedeps.Parse(in, bytes.NewBuffer(data)) 775 if err != nil { 776 return err 777 } 778 mergedDeps = append(mergedDeps, deps.Inputs...) 779 } 780 781 deps := makedeps.Deps{ 782 // Ninja doesn't care what the output file is, so we can use any string here. 783 Output: "outputfile", 784 Inputs: mergedDeps, 785 } 786 787 // Make the directory for the output depfile in case it is in a different directory 788 // than any of the output files. 789 outDir := filepath.Dir(out) 790 err := os.MkdirAll(outDir, 0777) 791 if err != nil { 792 return fmt.Errorf("failed to create %q: %w", outDir, err) 793 } 794 795 return ioutil.WriteFile(out, deps.Print(), 0666) 796} 797 798// joinPath wraps filepath.Join but returns file without appending to dir if file is 799// absolute. 800func joinPath(dir, file string) string { 801 if filepath.IsAbs(file) { 802 return file 803 } 804 return filepath.Join(dir, file) 805} 806 807// filesHaveSameContents compares the contents if two files, returning true if they are the same 808// and returning false if they are different or any errors occur. 809func filesHaveSameContents(a, b string) bool { 810 // Compare the sizes of the two files 811 statA, err := os.Stat(a) 812 if err != nil { 813 return false 814 } 815 statB, err := os.Stat(b) 816 if err != nil { 817 return false 818 } 819 820 if statA.Size() != statB.Size() { 821 return false 822 } 823 824 // Open the two files 825 fileA, err := os.Open(a) 826 if err != nil { 827 return false 828 } 829 defer fileA.Close() 830 fileB, err := os.Open(b) 831 if err != nil { 832 return false 833 } 834 defer fileB.Close() 835 836 // Compare the files 1MB at a time 837 const bufSize = 1 * 1024 * 1024 838 bufA := make([]byte, bufSize) 839 bufB := make([]byte, bufSize) 840 841 remain := statA.Size() 842 for remain > 0 { 843 toRead := int64(bufSize) 844 if toRead > remain { 845 toRead = remain 846 } 847 848 _, err = io.ReadFull(fileA, bufA[:toRead]) 849 if err != nil { 850 return false 851 } 852 _, err = io.ReadFull(fileB, bufB[:toRead]) 853 if err != nil { 854 return false 855 } 856 857 if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 { 858 return false 859 } 860 861 remain -= toRead 862 } 863 864 return true 865} 866 867func makeAbsPathEnv(pathEnv string) (string, error) { 868 pathEnvElements := filepath.SplitList(pathEnv) 869 for i, p := range pathEnvElements { 870 if !filepath.IsAbs(p) { 871 absPath, err := filepath.Abs(p) 872 if err != nil { 873 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err) 874 } 875 pathEnvElements[i] = absPath 876 } 877 } 878 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil 879} 880