xref: /aosp_15_r20/build/soong/cmd/sbox/sbox.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
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