• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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