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