blob: e3e68c99c727b8c145ccb5ac62299b84b20feeb0 [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 Gaston02a684b2017-10-27 14:59:27 -070035var usage = "Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> [--depfile-out depFile] <outputFile> [<outputFile>...]\n" +
Jeff Gaston193f2fb2017-06-12 15:00:12 -070036 "\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 {
Jeff Gaston02a684b2017-10-27 14:59:27 -070046 // the contents of the __SBOX_OUT_FILES__ variable
47 var outputsVarEntries []string
48 // all outputs
49 var allOutputs []string
50
Jeff Gastonefc1b412017-03-29 17:29:06 -070051 args := os.Args[1:]
52
53 var rawCommand string
54 var sandboxesRoot string
Jeff Gastonf49082a2017-06-07 13:22:22 -070055 removeTempDir := true
Jeff Gaston193f2fb2017-06-12 15:00:12 -070056 var outputRoot string
Jeff Gaston02a684b2017-10-27 14:59:27 -070057 var depfile string
Jeff Gastonefc1b412017-03-29 17:29:06 -070058
59 for i := 0; i < len(args); i++ {
60 arg := args[i]
61 if arg == "--sandbox-path" {
62 sandboxesRoot = args[i+1]
63 i++
64 } else if arg == "-c" {
65 rawCommand = args[i+1]
66 i++
Jeff Gaston193f2fb2017-06-12 15:00:12 -070067 } else if arg == "--output-root" {
68 outputRoot = args[i+1]
69 i++
70 } else if arg == "--keep-out-dir" {
71 removeTempDir = false
Jeff Gaston02a684b2017-10-27 14:59:27 -070072 } else if arg == "--depfile-out" {
73 depfile = args[i+1]
74 i++
Jeff Gastonefc1b412017-03-29 17:29:06 -070075 } else {
Jeff Gaston02a684b2017-10-27 14:59:27 -070076 outputsVarEntries = append(outputsVarEntries, arg)
Jeff Gastonefc1b412017-03-29 17:29:06 -070077 }
78 }
Jeff Gaston02a684b2017-10-27 14:59:27 -070079 if rawCommand == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -070080 return usageError("-c <commandToRun> is required and must be non-empty")
81 }
Jeff Gaston02a684b2017-10-27 14:59:27 -070082 if len(outputsVarEntries) == 0 {
Jeff Gastonefc1b412017-03-29 17:29:06 -070083 return usageError("at least one output file must be given")
84 }
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
91 return usageError("--sandbox-path <sandboxPath> is required and must be non-empty")
92 }
Jeff Gaston02a684b2017-10-27 14:59:27 -070093 if len(outputRoot) == 0 {
94 return usageError("--output-root <outputRoot> is required and must be non-empty")
Jeff Gaston193f2fb2017-06-12 15:00:12 -070095 }
96
Jeff Gastonefc1b412017-03-29 17:29:06 -070097 os.MkdirAll(sandboxesRoot, 0777)
98
99 tempDir, err := ioutil.TempDir(sandboxesRoot, "sbox")
Jeff Gaston02a684b2017-10-27 14:59:27 -0700100
101 // Rewrite output file paths to be relative to output root
102 // This facilitates matching them up against the corresponding paths in the temporary directory in case they're absolute
103 for i, filePath := range outputsVarEntries {
104 relativePath, err := filepath.Rel(outputRoot, filePath)
105 if err != nil {
106 return err
107 }
108 outputsVarEntries[i] = relativePath
109 }
110
111 allOutputs = append([]string(nil), outputsVarEntries...)
112
113 if depfile != "" {
114 sandboxedDepfile, err := filepath.Rel(outputRoot, depfile)
115 if err != nil {
116 return err
117 }
118 allOutputs = append(allOutputs, sandboxedDepfile)
119 if !strings.Contains(rawCommand, "__SBOX_DEPFILE__") {
120 return fmt.Errorf("the --depfile-out argument only makes sense if the command contains the text __SBOX_DEPFILE__")
121 }
122 rawCommand = strings.Replace(rawCommand, "__SBOX_DEPFILE__", filepath.Join(tempDir, sandboxedDepfile), -1)
123
124 }
125
Jeff Gastonefc1b412017-03-29 17:29:06 -0700126 if err != nil {
127 return fmt.Errorf("Failed to create temp dir: %s", err)
128 }
129
130 // In the common case, the following line of code is what removes the sandbox
131 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
132 // then at the beginning of the next build, Soong will retry the cleanup
Jeff Gastonf49082a2017-06-07 13:22:22 -0700133 defer func() {
134 // in some cases we decline to remove the temp dir, to facilitate debugging
135 if removeTempDir {
136 os.RemoveAll(tempDir)
137 }
138 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700139
140 if strings.Contains(rawCommand, "__SBOX_OUT_DIR__") {
141 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_DIR__", tempDir, -1)
142 }
143
144 if strings.Contains(rawCommand, "__SBOX_OUT_FILES__") {
145 // expands into a space-separated list of output files to be generated into the sandbox directory
146 tempOutPaths := []string{}
Jeff Gaston02a684b2017-10-27 14:59:27 -0700147 for _, outputPath := range outputsVarEntries {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700148 tempOutPath := path.Join(tempDir, outputPath)
149 tempOutPaths = append(tempOutPaths, tempOutPath)
150 }
151 pathsText := strings.Join(tempOutPaths, " ")
152 rawCommand = strings.Replace(rawCommand, "__SBOX_OUT_FILES__", pathsText, -1)
153 }
154
Jeff Gaston02a684b2017-10-27 14:59:27 -0700155 for _, filePath := range allOutputs {
156 dir := path.Join(tempDir, filepath.Dir(filePath))
157 err = os.MkdirAll(dir, 0777)
158 if err != nil {
159 return err
160 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700161 }
162
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700163 commandDescription := rawCommand
164
Jeff Gastonefc1b412017-03-29 17:29:06 -0700165 cmd := exec.Command("bash", "-c", rawCommand)
166 cmd.Stdin = os.Stdin
167 cmd.Stdout = os.Stdout
168 cmd.Stderr = os.Stderr
169 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700170
Jeff Gastonefc1b412017-03-29 17:29:06 -0700171 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700172 return fmt.Errorf("sbox command (%s) failed with err %#v\n", commandDescription, err.Error())
Jeff Gastonefc1b412017-03-29 17:29:06 -0700173 } else if err != nil {
174 return err
175 }
176
Jeff Gastonf49082a2017-06-07 13:22:22 -0700177 // validate that all files are created properly
178 var outputErrors []error
Jeff Gaston02a684b2017-10-27 14:59:27 -0700179 for _, filePath := range allOutputs {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700180 tempPath := filepath.Join(tempDir, filePath)
181 fileInfo, err := os.Stat(tempPath)
182 if err != nil {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700183 outputErrors = append(outputErrors, fmt.Errorf("failed to create expected output file: %s\n", tempPath))
184 continue
Jeff Gastonefc1b412017-03-29 17:29:06 -0700185 }
186 if fileInfo.IsDir() {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700187 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 -0700188 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700189 }
190 if len(outputErrors) > 0 {
191 // Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
192 // Soong will delete it later anyway.
193 removeTempDir = false
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700194 return fmt.Errorf("mismatch between declared and actual outputs in sbox command (%s):\n%v", commandDescription, outputErrors)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700195 }
196 // the created files match the declared files; now move them
Jeff Gaston02a684b2017-10-27 14:59:27 -0700197 for _, filePath := range allOutputs {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700198 tempPath := filepath.Join(tempDir, filePath)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700199 destPath := filePath
200 if len(outputRoot) != 0 {
201 destPath = filepath.Join(outputRoot, filePath)
202 }
203 err := os.Rename(tempPath, destPath)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700204 if err != nil {
205 return err
206 }
207 }
Jeff Gastonf49082a2017-06-07 13:22:22 -0700208
Jeff Gastonefc1b412017-03-29 17:29:06 -0700209 // TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
210 return nil
211}