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