blob: 558bc3fb71d219cfd8f2b07ca9a6ae721d4fd408 [file] [log] [blame]
Jeff Gastonefc1b412017-03-29 17:29:06 -07001// 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 (
Jeff Gaston93f0f372017-11-01 13:33:02 -070018 "flag"
Jeff Gastonefc1b412017-03-29 17:29:06 -070019 "fmt"
20 "io/ioutil"
21 "os"
22 "os/exec"
23 "path"
24 "path/filepath"
25 "strings"
26)
27
Jeff Gaston93f0f372017-11-01 13:33:02 -070028var (
29 sandboxesRoot string
30 rawCommand string
31 outputRoot string
32 keepOutDir bool
33 depfileOut string
34)
35
36func init() {
37 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
38 "root of temp directory to put the sandbox into")
39 flag.StringVar(&rawCommand, "c", "",
40 "command to run")
41 flag.StringVar(&outputRoot, "output-root", "",
42 "root of directory to copy outputs into")
43 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
44 "whether to keep the sandbox directory when done")
45
46 flag.StringVar(&depfileOut, "depfile-out", "",
47 "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__")
Jeff Gaston8a88db52017-11-06 13:33:14 -080048
Jeff Gaston93f0f372017-11-01 13:33:02 -070049}
50
51func usageViolation(violation string) {
52 if violation != "" {
53 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
54 }
55
56 fmt.Fprintf(os.Stderr,
Jeff Gaston8a88db52017-11-06 13:33:14 -080057 "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> --overwrite [--depfile-out depFile] <outputFile> [<outputFile>...]\n"+
Jeff Gaston93f0f372017-11-01 13:33:02 -070058 "\n"+
Jeff Gaston8a88db52017-11-06 13:33:14 -080059 "Deletes <outputRoot>,"+
60 "runs <commandToRun>,"+
61 "and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070062
63 flag.PrintDefaults()
64
65 os.Exit(1)
66}
67
Jeff Gastonefc1b412017-03-29 17:29:06 -070068func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070069 flag.Usage = func() {
70 usageViolation("")
71 }
72 flag.Parse()
73
Jeff Gastonefc1b412017-03-29 17:29:06 -070074 error := run()
75 if error != nil {
76 fmt.Fprintln(os.Stderr, error)
77 os.Exit(1)
78 }
79}
80
Jeff Gastonefc1b412017-03-29 17:29:06 -070081func run() error {
Jeff Gaston02a684b2017-10-27 14:59:27 -070082 if rawCommand == "" {
Jeff Gaston93f0f372017-11-01 13:33:02 -070083 usageViolation("-c <commandToRun> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -070084 }
Jeff Gaston02a684b2017-10-27 14:59:27 -070085 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -070086 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
87 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
88 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
89 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
90 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -070091 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -070092 }
Jeff Gaston02a684b2017-10-27 14:59:27 -070093 if len(outputRoot) == 0 {
Jeff Gaston93f0f372017-11-01 13:33:02 -070094 usageViolation("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -070095 }
96
Jeff Gaston93f0f372017-11-01 13:33:02 -070097 // the contents of the __SBOX_OUT_FILES__ variable
98 outputsVarEntries := flag.Args()
99 if len(outputsVarEntries) == 0 {
100 usageViolation("at least one output file must be given")
101 }
102
103 // all outputs
104 var allOutputs []string
105
Jeff Gaston8a88db52017-11-06 13:33:14 -0800106 // setup directories
107 err := os.MkdirAll(sandboxesRoot, 0777)
108 if err != nil {
109 return err
110 }
111 err = os.RemoveAll(outputRoot)
112 if err != nil {
113 return err
114 }
115 err = os.MkdirAll(outputRoot, 0777)
116 if err != nil {
117 return err
118 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700119
120 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700121
122 // Rewrite output file paths to be relative to output root
123 // This facilitates matching them up against the corresponding paths in the temporary directory in case they're absolute
124 for i, filePath := range outputsVarEntries {
125 relativePath, err := filepath.Rel(outputRoot, filePath)
126 if err != nil {
127 return err
128 }
129 outputsVarEntries[i] = relativePath
130 }
131
132 allOutputs = append([]string(nil), outputsVarEntries...)
133
Jeff Gaston93f0f372017-11-01 13:33:02 -0700134 if depfileOut != "" {
135 sandboxedDepfile, err := filepath.Rel(outputRoot, depfileOut)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700136 if err != nil {
137 return err
138 }
139 allOutputs = append(allOutputs, sandboxedDepfile)
140 if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") {
141 return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__")
142 }
143 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
144
145 }
146
Jeff Gastonefc1b412017-03-29 17:29:06 -0700147 if err != nil {
148 return fmt.Errorf("Failed to create temp dir: %s", err)
149 }
150
151 // In the common case, the following line of code is what removes the sandbox
152 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
153 // then at the beginning of the next build, Soong will retry the cleanup
Jeff Gastonf49082a2017-06-07 13:22:22 -0700154 defer func() {
155 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700156 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700157 os.RemoveAll(tempDir)
158 }
159 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700160
161 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
162 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
163 }
164
165 if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
166 // expands into a space-separated list of output files to be generated into the sandbox directory
167 tempOutPaths := []string{}
Jeff Gaston02a684b2017-10-27 14:59:27 -0700168 for _, outputPath := range outputsVarEntries {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700169 tempOutPath := path.Join(tempDir, outputPath)
170 tempOutPaths = append(tempOutPaths, tempOutPath)
171 }
172 pathsText := strings.Join(tempOutPaths, " ")
173 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
174 }
175
Jeff Gaston02a684b2017-10-27 14:59:27 -0700176 for _, filePath := range allOutputs {
177 dir := path.Join(tempDir, filepath.Dir(filePath))
178 err = os.MkdirAll(dir, 0777)
179 if err != nil {
180 return err
181 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700182 }
183
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700184 commandDescription := rawCommand
185
Jeff Gastonefc1b412017-03-29 17:29:06 -0700186 cmd := exec.Command("bash", "-c", rawCommand)
187 cmd.Stdin = os.Stdin
188 cmd.Stdout = os.Stdout
189 cmd.Stderr = os.Stderr
190 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700191
Jeff Gastonefc1b412017-03-29 17:29:06 -0700192 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700193 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700194 } else if err != nil {
195 return err
196 }
197
Jeff Gastonf49082a2017-06-07 13:22:22 -0700198 // validate that all files are created properly
199 var outputErrors []error
Jeff Gaston02a684b2017-10-27 14:59:27 -0700200 for _, filePath := range allOutputs {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700201 tempPath := filepath.Join(tempDir, filePath)
202 fileInfo, err := os.Stat(tempPath)
203 if err != nil {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700204 outputErrors = append(outputErrors, fmt.Errorf("failed to create expected output file: %s\n", tempPath))
205 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700206 }
207 if fileInfo.IsDir() {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700208 outputErrors = append(outputErrors, fmt.Errorf("Output path %s refers to a directory, not a file. This is not permitted because it prevents robust up-to-date checks\n", filePath))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700209 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700210 }
211 if len(outputErrors) > 0 {
212 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
213 // Soong will delete it later anyway.
Jeff Gaston93f0f372017-11-01 13:33:02 -0700214 keepOutDir = true
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700215 return fmt.Errorf("mismatch between declared and actual outputs in sbox command (%s):\n%v", commandDescription, outputErrors)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700216 }
217 // the created files match the declared files; now move them
Jeff Gaston02a684b2017-10-27 14:59:27 -0700218 for _, filePath := range allOutputs {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700219 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700220 destPath := filePath
221 if len(outputRoot) != 0 {
222 destPath = filepath.Join(outputRoot, filePath)
223 }
Jeff Gaston8a88db52017-11-06 13:33:14 -0800224 err := os.MkdirAll(filepath.Dir(destPath), 0777)
225 if err != nil {
226 return err
227 }
228 err = os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700229 if err != nil {
230 return err
231 }
232 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700233
Jeff Gastonefc1b412017-03-29 17:29:06 -0700234 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
235 return nil
236}