blob: db483f1681e9abd6f31b16db088cf0ceafeb066e [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 (
Dan Willemsenc89b6f12019-08-29 14:47:40 -070018 "bytes"
Jeff Gaston90cfb092017-09-26 16:46:10 -070019 "errors"
Jeff Gaston93f0f372017-11-01 13:33:02 -070020 "flag"
Jeff Gastonefc1b412017-03-29 17:29:06 -070021 "fmt"
Colin Crosse16ce362020-11-12 08:29:30 -080022 "io"
Jeff Gastonefc1b412017-03-29 17:29:06 -070023 "io/ioutil"
24 "os"
25 "os/exec"
Jeff Gastonefc1b412017-03-29 17:29:06 -070026 "path/filepath"
Colin Crosse16ce362020-11-12 08:29:30 -080027 "strconv"
Jeff Gastonefc1b412017-03-29 17:29:06 -070028 "strings"
Colin Crossd1c1e6f2019-03-29 13:54:39 -070029 "time"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070030
Colin Crosse16ce362020-11-12 08:29:30 -080031 "android/soong/cmd/sbox/sbox_proto"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070032 "android/soong/makedeps"
Colin Crosse16ce362020-11-12 08:29:30 -080033
34 "github.com/golang/protobuf/proto"
Jeff Gastonefc1b412017-03-29 17:29:06 -070035)
36
Jeff Gaston93f0f372017-11-01 13:33:02 -070037var (
38 sandboxesRoot string
Colin Crosse16ce362020-11-12 08:29:30 -080039 manifestFile string
Jeff Gaston93f0f372017-11-01 13:33:02 -070040 keepOutDir bool
Colin Crosse16ce362020-11-12 08:29:30 -080041)
42
43const (
44 depFilePlaceholder = "__SBOX_DEPFILE__"
45 sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__"
Jeff Gaston93f0f372017-11-01 13:33:02 -070046)
47
48func init() {
49 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
50 "root of temp directory to put the sandbox into")
Colin Crosse16ce362020-11-12 08:29:30 -080051 flag.StringVar(&manifestFile, "manifest", "",
52 "textproto manifest describing the sandboxed command(s)")
Jeff Gaston93f0f372017-11-01 13:33:02 -070053 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
54 "whether to keep the sandbox directory when done")
Jeff Gaston93f0f372017-11-01 13:33:02 -070055}
56
57func usageViolation(violation string) {
58 if violation != "" {
59 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
60 }
61
62 fmt.Fprintf(os.Stderr,
Colin Crosse16ce362020-11-12 08:29:30 -080063 "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070064
65 flag.PrintDefaults()
66
67 os.Exit(1)
68}
69
Jeff Gastonefc1b412017-03-29 17:29:06 -070070func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070071 flag.Usage = func() {
72 usageViolation("")
73 }
74 flag.Parse()
75
Jeff Gastonefc1b412017-03-29 17:29:06 -070076 error := run()
77 if error != nil {
78 fmt.Fprintln(os.Stderr, error)
79 os.Exit(1)
80 }
81}
82
Jeff Gaston90cfb092017-09-26 16:46:10 -070083func findAllFilesUnder(root string) (paths []string) {
84 paths = []string{}
85 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
86 if !info.IsDir() {
87 relPath, err := filepath.Rel(root, path)
88 if err != nil {
89 // couldn't find relative path from ancestor?
90 panic(err)
91 }
92 paths = append(paths, relPath)
93 }
94 return nil
95 })
96 return paths
97}
98
Jeff Gastonefc1b412017-03-29 17:29:06 -070099func run() error {
Colin Crosse16ce362020-11-12 08:29:30 -0800100 if manifestFile == "" {
101 usageViolation("--manifest <manifest> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700102 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700103 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700104 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
105 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
106 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
107 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
108 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700109 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700110 }
Colin Crosse16ce362020-11-12 08:29:30 -0800111
112 manifest, err := readManifest(manifestFile)
113
114 if len(manifest.Commands) == 0 {
115 return fmt.Errorf("at least one commands entry is required in %q", manifestFile)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700116 }
117
Colin Crosse16ce362020-11-12 08:29:30 -0800118 // setup sandbox directory
119 err = os.MkdirAll(sandboxesRoot, 0777)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800120 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800121 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800122 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700123
124 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
125 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800126 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700127 }
128
129 // In the common case, the following line of code is what removes the sandbox
130 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
Colin Crosse16ce362020-11-12 08:29:30 -0800131 // then at the beginning of the next build, Soong will wipe the temporary
132 // directory.
Jeff Gastonf49082a2017-06-07 13:22:22 -0700133 defer func() {
134 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700135 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700136 os.RemoveAll(tempDir)
137 }
138 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700139
Colin Crosse16ce362020-11-12 08:29:30 -0800140 // If there is more than one command in the manifest use a separate directory for each one.
141 useSubDir := len(manifest.Commands) > 1
142 var commandDepFiles []string
Jeff Gastonefc1b412017-03-29 17:29:06 -0700143
Colin Crosse16ce362020-11-12 08:29:30 -0800144 for i, command := range manifest.Commands {
145 localTempDir := tempDir
146 if useSubDir {
147 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700148 }
Colin Crosse16ce362020-11-12 08:29:30 -0800149 depFile, err := runCommand(command, localTempDir)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700150 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800151 // Running the command failed, keep the temporary output directory around in
152 // case a user wants to inspect it for debugging purposes. Soong will delete
153 // it at the beginning of the next build anyway.
154 keepOutDir = true
Jeff Gaston02a684b2017-10-27 14:59:27 -0700155 return err
156 }
Colin Crosse16ce362020-11-12 08:29:30 -0800157 if depFile != "" {
158 commandDepFiles = append(commandDepFiles, depFile)
159 }
160 }
161
162 outputDepFile := manifest.GetOutputDepfile()
163 if len(commandDepFiles) > 0 && outputDepFile == "" {
164 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file",
165 depFilePlaceholder)
166 }
167
168 if outputDepFile != "" {
169 // Merge the depfiles from each command in the manifest to a single output depfile.
170 err = rewriteDepFiles(commandDepFiles, outputDepFile)
171 if err != nil {
172 return fmt.Errorf("failed merging depfiles: %w", err)
173 }
174 }
175
176 return nil
177}
178
179// readManifest reads an sbox manifest from a textproto file.
180func readManifest(file string) (*sbox_proto.Manifest, error) {
181 manifestData, err := ioutil.ReadFile(file)
182 if err != nil {
183 return nil, fmt.Errorf("error reading manifest %q: %w", file, err)
184 }
185
186 manifest := sbox_proto.Manifest{}
187
188 err = proto.UnmarshalText(string(manifestData), &manifest)
189 if err != nil {
190 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err)
191 }
192
193 return &manifest, nil
194}
195
196// runCommand runs a single command from a manifest. If the command references the
197// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
198func runCommand(command *sbox_proto.Command, tempDir string) (depFile string, err error) {
199 rawCommand := command.GetCommand()
200 if rawCommand == "" {
201 return "", fmt.Errorf("command is required")
202 }
203
204 err = os.MkdirAll(tempDir, 0777)
205 if err != nil {
206 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
207 }
208
209 // Copy in any files specified by the manifest.
210 err = linkOrCopyFiles(command.CopyBefore, "", tempDir)
211 if err != nil {
212 return "", err
213 }
214
215 if strings.Contains(rawCommand, depFilePlaceholder) {
216 depFile = filepath.Join(tempDir, "deps.d")
217 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
218 }
219
220 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
221 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, tempDir, -1)
222 }
223
224 // Emulate ninja's behavior of creating the directories for any output files before
225 // running the command.
226 err = makeOutputDirs(command.CopyAfter, tempDir)
227 if err != nil {
228 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700229 }
230
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700231 commandDescription := rawCommand
232
Jeff Gastonefc1b412017-03-29 17:29:06 -0700233 cmd := exec.Command("bash", "-c", rawCommand)
234 cmd.Stdin = os.Stdin
235 cmd.Stdout = os.Stdout
236 cmd.Stderr = os.Stderr
Colin Crosse16ce362020-11-12 08:29:30 -0800237
238 if command.GetChdir() {
239 cmd.Dir = tempDir
240 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700241 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700242
Jeff Gastonefc1b412017-03-29 17:29:06 -0700243 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Crosse16ce362020-11-12 08:29:30 -0800244 return "", fmt.Errorf("sbox command failed with err:\n%s\n%w\n", commandDescription, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700245 } else if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800246 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700247 }
248
Colin Crosse16ce362020-11-12 08:29:30 -0800249 missingOutputErrors := validateOutputFiles(command.CopyAfter, tempDir)
250
Jeff Gaston90cfb092017-09-26 16:46:10 -0700251 if len(missingOutputErrors) > 0 {
252 // find all created files for making a more informative error message
253 createdFiles := findAllFilesUnder(tempDir)
254
255 // build error message
256 errorMessage := "mismatch between declared and actual outputs\n"
257 errorMessage += "in sbox command(" + commandDescription + ")\n\n"
258 errorMessage += "in sandbox " + tempDir + ",\n"
259 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
260 for _, missingOutputError := range missingOutputErrors {
Colin Crosse16ce362020-11-12 08:29:30 -0800261 errorMessage += " " + missingOutputError.Error() + "\n"
Jeff Gaston90cfb092017-09-26 16:46:10 -0700262 }
263 if len(createdFiles) < 1 {
264 errorMessage += "created 0 files."
265 } else {
266 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
267 creationMessages := createdFiles
268 maxNumCreationLines := 10
269 if len(creationMessages) > maxNumCreationLines {
270 creationMessages = creationMessages[:maxNumCreationLines]
271 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
272 }
273 for _, creationMessage := range creationMessages {
274 errorMessage += " " + creationMessage + "\n"
275 }
276 }
277
Colin Crosse16ce362020-11-12 08:29:30 -0800278 return "", errors.New(errorMessage)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700279 }
280 // the created files match the declared files; now move them
Colin Crosse16ce362020-11-12 08:29:30 -0800281 err = moveFiles(command.CopyAfter, tempDir, "")
282
283 return depFile, nil
284}
285
286// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
287// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
288// so that the tools don't have to.
289func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
290 for _, copyPair := range copies {
291 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
292 err := os.MkdirAll(dir, 0777)
293 if err != nil {
294 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700295 }
Colin Crosse16ce362020-11-12 08:29:30 -0800296 }
297 return nil
298}
299
300// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
301// were created by the command.
302func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir string) []error {
303 var missingOutputErrors []error
304 for _, copyPair := range copies {
305 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
306 fileInfo, err := os.Stat(fromPath)
307 if err != nil {
308 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
309 continue
310 }
311 if fileInfo.IsDir() {
312 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
313 }
314 }
315 return missingOutputErrors
316}
317
318// linkOrCopyFiles hardlinks or copies files in or out of the sandbox.
319func linkOrCopyFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error {
320 for _, copyPair := range copies {
321 fromPath := joinPath(fromDir, copyPair.GetFrom())
322 toPath := joinPath(toDir, copyPair.GetTo())
323 err := linkOrCopyOneFile(fromPath, toPath)
324 if err != nil {
325 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
326 }
327 }
328 return nil
329}
330
331// linkOrCopyOneFile first attempts to hardlink a file to a destination, and falls back to making
332// a copy if the hardlink fails.
333func linkOrCopyOneFile(from string, to string) error {
334 err := os.MkdirAll(filepath.Dir(to), 0777)
335 if err != nil {
336 return err
337 }
338
339 // First try hardlinking
340 err = os.Link(from, to)
341 if err != nil {
342 // Retry with copying in case the source an destination are on different filesystems.
343 // TODO: check for specific hardlink error?
344 err = copyOneFile(from, to)
345 if err != nil {
346 return err
347 }
348 }
349
350 return nil
351}
352
353// copyOneFile copies a file.
354func copyOneFile(from string, to string) error {
355 stat, err := os.Stat(from)
356 if err != nil {
357 return err
358 }
359
360 perm := stat.Mode()
361
362 in, err := os.Open(from)
363 if err != nil {
364 return err
365 }
366 defer in.Close()
367
368 out, err := os.Create(to)
369 if err != nil {
370 return err
371 }
372 defer func() {
373 out.Close()
374 if err != nil {
375 os.Remove(to)
376 }
377 }()
378
379 _, err = io.Copy(out, in)
380 if err != nil {
381 return err
382 }
383
384 if err = out.Close(); err != nil {
385 return err
386 }
387
388 if err = os.Chmod(to, perm); err != nil {
389 return err
390 }
391
392 return nil
393}
394
395// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
396// to moving files where the source and destination are in the same filesystem. This is OK for
397// sbox because the temporary directory is inside the out directory. It updates the timestamp
398// of the new file.
399func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error {
400 for _, copyPair := range copies {
401 fromPath := joinPath(fromDir, copyPair.GetFrom())
402 toPath := joinPath(toDir, copyPair.GetTo())
403 err := os.MkdirAll(filepath.Dir(toPath), 0777)
404 if err != nil {
405 return err
406 }
407
408 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800409 if err != nil {
410 return err
411 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700412
413 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
414 // files with old timestamps).
415 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800416 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700417 if err != nil {
418 return err
419 }
420 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700421 return nil
422}
Colin Crosse16ce362020-11-12 08:29:30 -0800423
424// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
425// to an output file.
426func rewriteDepFiles(ins []string, out string) error {
427 var mergedDeps []string
428 for _, in := range ins {
429 data, err := ioutil.ReadFile(in)
430 if err != nil {
431 return err
432 }
433
434 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
435 if err != nil {
436 return err
437 }
438 mergedDeps = append(mergedDeps, deps.Inputs...)
439 }
440
441 deps := makedeps.Deps{
442 // Ninja doesn't care what the output file is, so we can use any string here.
443 Output: "outputfile",
444 Inputs: mergedDeps,
445 }
446
447 // Make the directory for the output depfile in case it is in a different directory
448 // than any of the output files.
449 outDir := filepath.Dir(out)
450 err := os.MkdirAll(outDir, 0777)
451 if err != nil {
452 return fmt.Errorf("failed to create %q: %w", outDir, err)
453 }
454
455 return ioutil.WriteFile(out, deps.Print(), 0666)
456}
457
458// joinPath wraps filepath.Join but returns file without appending to dir if file is
459// absolute.
460func joinPath(dir, file string) string {
461 if filepath.IsAbs(file) {
462 return file
463 }
464 return filepath.Join(dir, file)
465}