blob: 5064626804f20f59bebfbdf8ede0c1ce14ad943e [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 (
18 "fmt"
19 "io/ioutil"
20 "os"
21 "os/exec"
22 "path"
23 "path/filepath"
24 "strings"
25)
26
27func main() {
28 error := run()
29 if error != nil {
30 fmt.Fprintln(os.Stderr, error)
31 os.Exit(1)
32 }
33}
34
Jeff Gaston193f2fb2017-06-12 15:00:12 -070035var usage = "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> <outputFile> [<outputFile>...]\n" +
36 "\n" +
37 "Runs <commandToRun> and moves each <outputFile> out of <sandboxPath>\n" +
38 "If any file in <outputFiles> is specified by absolute path, then <outputRoot> must be specified as well,\n" +
39 "to enable sbox to compute the relative path within the sandbox of the specified output files"
Jeff Gastonefc1b412017-03-29 17:29:06 -070040
41func usageError(violation string) error {
Jeff Gaston193f2fb2017-06-12 15:00:12 -070042 return fmt.Errorf("Usage error: %s.\n\n%s", violation, usage)
Jeff Gastonefc1b412017-03-29 17:29:06 -070043}
44
45func run() error {
46 var outFiles []string
47 args := os.Args[1:]
48
49 var rawCommand string
50 var sandboxesRoot string
Jeff Gastonf49082a2017-06-07 13:22:22 -070051 removeTempDir := true
Jeff Gaston193f2fb2017-06-12 15:00:12 -070052 var outputRoot string
Jeff Gastonefc1b412017-03-29 17:29:06 -070053
54 for i := 0; i < len(args); i++ {
55 arg := args[i]
56 if arg == "--sandbox-path" {
57 sandboxesRoot = args[i+1]
58 i++
59 } else if arg == "-c" {
60 rawCommand = args[i+1]
61 i++
Jeff Gaston193f2fb2017-06-12 15:00:12 -070062 } else if arg == "--output-root" {
63 outputRoot = args[i+1]
64 i++
65 } else if arg == "--keep-out-dir" {
66 removeTempDir = false
Jeff Gastonefc1b412017-03-29 17:29:06 -070067 } else {
68 outFiles = append(outFiles, arg)
69 }
70 }
71 if len(rawCommand) == 0 {
72 return usageError("-c <commandToRun> is required and must be non-empty")
73 }
74 if outFiles == nil {
75 return usageError("at least one output file must be given")
76 }
77 if len(sandboxesRoot) == 0 {
78 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
79 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
80 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
81 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
82 // and by passing it as a parameter we don't need to duplicate its value
83 return usageError("--sandbox-path <sandboxPath> is required and must be non-empty")
84 }
85
Jeff Gaston193f2fb2017-06-12 15:00:12 -070086 // Rewrite output file paths to be relative to output root
87 // This facilitates matching them up against the corresponding paths in the temporary directory in case they're absolute
88 for i, filePath := range outFiles {
89 if path.IsAbs(filePath) {
90 if len(outputRoot) == 0 {
91 return fmt.Errorf("Absolute path %s requires nonempty value for --output-root", filePath)
92 }
93 }
94 relativePath, err := filepath.Rel(outputRoot, filePath)
95 if err != nil {
96 return err
97 }
98 outFiles[i] = relativePath
99 }
100
Jeff Gastonefc1b412017-03-29 17:29:06 -0700101 os.MkdirAll(sandboxesRoot, 0777)
102
103 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
104 if err != nil {
105 return fmt.Errorf("Failed to create temp dir: %s", err)
106 }
107
108 // In the common case, the following line of code is what removes the sandbox
109 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
110 // then at the beginning of the next build, Soong will retry the cleanup
Jeff Gastonf49082a2017-06-07 13:22:22 -0700111 defer func() {
112 // in some cases we decline to remove the temp dir, to facilitate debugging
113 if removeTempDir {
114 os.RemoveAll(tempDir)
115 }
116 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700117
118 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
119 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
120 }
121
122 if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
123 // expands into a space-separated list of output files to be generated into the sandbox directory
124 tempOutPaths := []string{}
125 for _, outputPath := range outFiles {
126 tempOutPath := path.Join(tempDir, outputPath)
127 tempOutPaths = append(tempOutPaths, tempOutPath)
128 }
129 pathsText := strings.Join(tempOutPaths, " ")
130 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
131 }
132
133 for _, filePath := range outFiles {
134 os.MkdirAll(path.Join(tempDir, filepath.Dir(filePath)), 0777)
135 }
136
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700137 commandDescription := rawCommand
138
Jeff Gastonefc1b412017-03-29 17:29:06 -0700139 cmd := exec.Command("bash", "-c", rawCommand)
140 cmd.Stdin = os.Stdin
141 cmd.Stdout = os.Stdout
142 cmd.Stderr = os.Stderr
143 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700144
Jeff Gastonefc1b412017-03-29 17:29:06 -0700145 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700146 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700147 } else if err != nil {
148 return err
149 }
150
Jeff Gastonf49082a2017-06-07 13:22:22 -0700151 // validate that all files are created properly
152 var outputErrors []error
Jeff Gastonefc1b412017-03-29 17:29:06 -0700153 for _, filePath := range outFiles {
154 tempPath := filepath.Join(tempDir, filePath)
155 fileInfo, err := os.Stat(tempPath)
156 if err != nil {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700157 outputErrors = append(outputErrors, fmt.Errorf("failed to create expected output file: %s\n", tempPath))
158 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700159 }
160 if fileInfo.IsDir() {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700161 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 -0700162 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700163 }
164 if len(outputErrors) > 0 {
165 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
166 // Soong will delete it later anyway.
167 removeTempDir = false
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700168 return fmt.Errorf("mismatch between declared and actual outputs in sbox command (%s):\n%v", commandDescription, outputErrors)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700169 }
170 // the created files match the declared files; now move them
171 for _, filePath := range outFiles {
172 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700173 destPath := filePath
174 if len(outputRoot) != 0 {
175 destPath = filepath.Join(outputRoot, filePath)
176 }
177 err := os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700178 if err != nil {
179 return err
180 }
181 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700182
Jeff Gastonefc1b412017-03-29 17:29:06 -0700183 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
184 return nil
185}