github.com/westcoastroms/westcoastroms-build@v0.0.0-20190928114312-2350e5a73030/build/soong/cmd/sbox/sbox.go (about)

     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  
    15  package main
    16  
    17  import (
    18  	"errors"
    19  	"flag"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"os/exec"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  )
    28  
    29  var (
    30  	sandboxesRoot string
    31  	rawCommand    string
    32  	outputRoot    string
    33  	keepOutDir    bool
    34  	depfileOut    string
    35  )
    36  
    37  func init() {
    38  	flag.StringVar(&sandboxesRoot, "sandbox-path", "",
    39  		"root of temp directory to put the sandbox into")
    40  	flag.StringVar(&rawCommand, "c", "",
    41  		"command to run")
    42  	flag.StringVar(&outputRoot, "output-root", "",
    43  		"root of directory to copy outputs into")
    44  	flag.BoolVar(&keepOutDir, "keep-out-dir", false,
    45  		"whether to keep the sandbox directory when done")
    46  
    47  	flag.StringVar(&depfileOut, "depfile-out", "",
    48  		"file path of the depfile to generate. This value will replace '__SBOX_DEPFILE__' in the command and will be treated as an output but won't be added to __SBOX_OUT_FILES__")
    49  
    50  }
    51  
    52  func usageViolation(violation string) {
    53  	if violation != "" {
    54  		fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
    55  	}
    56  
    57  	fmt.Fprintf(os.Stderr,
    58  		"Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> --overwrite [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+
    59  			"\n"+
    60  			"Deletes <outputRoot>,"+
    61  			"runs <commandToRun>,"+
    62  			"and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
    63  
    64  	flag.PrintDefaults()
    65  
    66  	os.Exit(1)
    67  }
    68  
    69  func main() {
    70  	flag.Usage = func() {
    71  		usageViolation("")
    72  	}
    73  	flag.Parse()
    74  
    75  	error := run()
    76  	if error != nil {
    77  		fmt.Fprintln(os.Stderr, error)
    78  		os.Exit(1)
    79  	}
    80  }
    81  
    82  func findAllFilesUnder(root string) (paths []string) {
    83  	paths = []string{}
    84  	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
    85  		if !info.IsDir() {
    86  			relPath, err := filepath.Rel(root, path)
    87  			if err != nil {
    88  				// couldn't find relative path from ancestor?
    89  				panic(err)
    90  			}
    91  			paths = append(paths, relPath)
    92  		}
    93  		return nil
    94  	})
    95  	return paths
    96  }
    97  
    98  func run() error {
    99  	if rawCommand == "" {
   100  		usageViolation("-c <commandToRun> is required and must be non-empty")
   101  	}
   102  	if sandboxesRoot == "" {
   103  		// In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
   104  		// and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
   105  		// the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
   106  		// However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
   107  		// and by passing it as a parameter we don't need to duplicate its value
   108  		usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
   109  	}
   110  	if len(outputRoot) == 0 {
   111  		usageViolation("--output-root <outputRoot> is required and must be non-empty")
   112  	}
   113  
   114  	// the contents of the __SBOX_OUT_FILES__ variable
   115  	outputsVarEntries := flag.Args()
   116  	if len(outputsVarEntries) == 0 {
   117  		usageViolation("at least one output file must be given")
   118  	}
   119  
   120  	// all outputs
   121  	var allOutputs []string
   122  
   123  	// setup directories
   124  	err := os.MkdirAll(sandboxesRoot, 0777)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	err = os.RemoveAll(outputRoot)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	err = os.MkdirAll(outputRoot, 0777)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
   138  
   139  	for i, filePath := range outputsVarEntries {
   140  		if !strings.HasPrefix(filePath, "__SBOX_OUT_DIR__/") {
   141  			return fmt.Errorf("output files must start with `__SBOX_OUT_DIR__/`")
   142  		}
   143  		outputsVarEntries[i] = strings.TrimPrefix(filePath, "__SBOX_OUT_DIR__/")
   144  	}
   145  
   146  	allOutputs = append([]string(nil), outputsVarEntries...)
   147  
   148  	if depfileOut != "" {
   149  		sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		allOutputs = append(allOutputs, sandboxedDepfile)
   154  		if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") {
   155  			return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__")
   156  		}
   157  		rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
   158  
   159  	}
   160  
   161  	if err != nil {
   162  		return fmt.Errorf("Failed to create temp dir: %s", err)
   163  	}
   164  
   165  	// In the common case, the following line of code is what removes the sandbox
   166  	// If a fatal error occurs (such as if our Go process is killed unexpectedly),
   167  	// then at the beginning of the next build, Soong will retry the cleanup
   168  	defer func() {
   169  		// in some cases we decline to remove the temp dir, to facilitate debugging
   170  		if !keepOutDir {
   171  			os.RemoveAll(tempDir)
   172  		}
   173  	}()
   174  
   175  	if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
   176  		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
   177  	}
   178  
   179  	if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
   180  		// expands into a space-separated list of output files to be generated into the sandbox directory
   181  		tempOutPaths := []string{}
   182  		for _, outputPath := range outputsVarEntries {
   183  			tempOutPath := path.Join(tempDir, outputPath)
   184  			tempOutPaths = append(tempOutPaths, tempOutPath)
   185  		}
   186  		pathsText := strings.Join(tempOutPaths, " ")
   187  		rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
   188  	}
   189  
   190  	for _, filePath := range allOutputs {
   191  		dir := path.Join(tempDir, filepath.Dir(filePath))
   192  		err = os.MkdirAll(dir, 0777)
   193  		if err != nil {
   194  			return err
   195  		}
   196  	}
   197  
   198  	commandDescription := rawCommand
   199  
   200  	cmd := exec.Command("bash", "-c", rawCommand)
   201  	cmd.Stdin = os.Stdin
   202  	cmd.Stdout = os.Stdout
   203  	cmd.Stderr = os.Stderr
   204  	err = cmd.Run()
   205  
   206  	if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
   207  		return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
   208  	} else if err != nil {
   209  		return err
   210  	}
   211  
   212  	// validate that all files are created properly
   213  	var missingOutputErrors []string
   214  	for _, filePath := range allOutputs {
   215  		tempPath := filepath.Join(tempDir, filePath)
   216  		fileInfo, err := os.Stat(tempPath)
   217  		if err != nil {
   218  			missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: does not exist", filePath))
   219  			continue
   220  		}
   221  		if fileInfo.IsDir() {
   222  			missingOutputErrors = append(missingOutputErrors, fmt.Sprintf("%s: not a file", filePath))
   223  		}
   224  	}
   225  	if len(missingOutputErrors) > 0 {
   226  		// find all created files for making a more informative error message
   227  		createdFiles := findAllFilesUnder(tempDir)
   228  
   229  		// build error message
   230  		errorMessage := "mismatch between declared and actual outputs\n"
   231  		errorMessage += "in sbox command(" + commandDescription + ")\n\n"
   232  		errorMessage += "in sandbox " + tempDir + ",\n"
   233  		errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
   234  		for _, missingOutputError := range missingOutputErrors {
   235  			errorMessage += "  " + missingOutputError + "\n"
   236  		}
   237  		if len(createdFiles) < 1 {
   238  			errorMessage += "created 0 files."
   239  		} else {
   240  			errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
   241  			creationMessages := createdFiles
   242  			maxNumCreationLines := 10
   243  			if len(creationMessages) > maxNumCreationLines {
   244  				creationMessages = creationMessages[:maxNumCreationLines]
   245  				creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
   246  			}
   247  			for _, creationMessage := range creationMessages {
   248  				errorMessage += "  " + creationMessage + "\n"
   249  			}
   250  		}
   251  
   252  		// Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
   253  		// Soong will delete it later anyway.
   254  		keepOutDir = true
   255  		return errors.New(errorMessage)
   256  	}
   257  	// the created files match the declared files; now move them
   258  	for _, filePath := range allOutputs {
   259  		tempPath := filepath.Join(tempDir, filePath)
   260  		destPath := filePath
   261  		if len(outputRoot) != 0 {
   262  			destPath = filepath.Join(outputRoot, filePath)
   263  		}
   264  		err := os.MkdirAll(filepath.Dir(destPath), 0777)
   265  		if err != nil {
   266  			return err
   267  		}
   268  		err = os.Rename(tempPath, destPath)
   269  		if err != nil {
   270  			return err
   271  		}
   272  	}
   273  
   274  	// TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
   275  	return nil
   276  }