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