| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 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 ( | 
| Dan Willemsen | c89b6f1 | 2019-08-29 14:47:40 -0700 | [diff] [blame] | 18 | "bytes" | 
| Ulf Adams | b73daa5 | 2020-11-25 23:09:09 +0100 | [diff] [blame] | 19 | "crypto/sha1" | 
|  | 20 | "encoding/hex" | 
| Jeff Gaston | 90cfb09 | 2017-09-26 16:46:10 -0700 | [diff] [blame] | 21 | "errors" | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 22 | "flag" | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 23 | "fmt" | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 24 | "io" | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 25 | "io/ioutil" | 
|  | 26 | "os" | 
|  | 27 | "os/exec" | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 28 | "path/filepath" | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 29 | "strconv" | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 30 | "strings" | 
| Colin Cross | d1c1e6f | 2019-03-29 13:54:39 -0700 | [diff] [blame] | 31 | "time" | 
| Dan Willemsen | c89b6f1 | 2019-08-29 14:47:40 -0700 | [diff] [blame] | 32 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 33 | "android/soong/cmd/sbox/sbox_proto" | 
| Dan Willemsen | c89b6f1 | 2019-08-29 14:47:40 -0700 | [diff] [blame] | 34 | "android/soong/makedeps" | 
| Colin Cross | e55bd42 | 2021-03-23 13:44:30 -0700 | [diff] [blame] | 35 | "android/soong/response" | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 36 |  | 
|  | 37 | "github.com/golang/protobuf/proto" | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 38 | ) | 
|  | 39 |  | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 40 | var ( | 
|  | 41 | sandboxesRoot string | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 42 | manifestFile  string | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 43 | keepOutDir    bool | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 44 | ) | 
|  | 45 |  | 
|  | 46 | const ( | 
|  | 47 | depFilePlaceholder    = "__SBOX_DEPFILE__" | 
|  | 48 | sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__" | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 49 | ) | 
|  | 50 |  | 
|  | 51 | func init() { | 
|  | 52 | flag.StringVar(&sandboxesRoot, "sandbox-path", "", | 
|  | 53 | "root of temp directory to put the sandbox into") | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 54 | flag.StringVar(&manifestFile, "manifest", "", | 
|  | 55 | "textproto manifest describing the sandboxed command(s)") | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 56 | flag.BoolVar(&keepOutDir, "keep-out-dir", false, | 
|  | 57 | "whether to keep the sandbox directory when done") | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 58 | } | 
|  | 59 |  | 
|  | 60 | func usageViolation(violation string) { | 
|  | 61 | if violation != "" { | 
|  | 62 | fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation) | 
|  | 63 | } | 
|  | 64 |  | 
|  | 65 | fmt.Fprintf(os.Stderr, | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 66 | "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n") | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 67 |  | 
|  | 68 | flag.PrintDefaults() | 
|  | 69 |  | 
|  | 70 | os.Exit(1) | 
|  | 71 | } | 
|  | 72 |  | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 73 | func main() { | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 74 | flag.Usage = func() { | 
|  | 75 | usageViolation("") | 
|  | 76 | } | 
|  | 77 | flag.Parse() | 
|  | 78 |  | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 79 | error := run() | 
|  | 80 | if error != nil { | 
|  | 81 | fmt.Fprintln(os.Stderr, error) | 
|  | 82 | os.Exit(1) | 
|  | 83 | } | 
|  | 84 | } | 
|  | 85 |  | 
| Jeff Gaston | 90cfb09 | 2017-09-26 16:46:10 -0700 | [diff] [blame] | 86 | func findAllFilesUnder(root string) (paths []string) { | 
|  | 87 | paths = []string{} | 
|  | 88 | filepath.Walk(root, func(path string, info os.FileInfo, err error) error { | 
|  | 89 | if !info.IsDir() { | 
|  | 90 | relPath, err := filepath.Rel(root, path) | 
|  | 91 | if err != nil { | 
|  | 92 | // couldn't find relative path from ancestor? | 
|  | 93 | panic(err) | 
|  | 94 | } | 
|  | 95 | paths = append(paths, relPath) | 
|  | 96 | } | 
|  | 97 | return nil | 
|  | 98 | }) | 
|  | 99 | return paths | 
|  | 100 | } | 
|  | 101 |  | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 102 | func run() error { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 103 | if manifestFile == "" { | 
|  | 104 | usageViolation("--manifest <manifest> is required and must be non-empty") | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 105 | } | 
| Jeff Gaston | 02a684b | 2017-10-27 14:59:27 -0700 | [diff] [blame] | 106 | if sandboxesRoot == "" { | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 107 | // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR, | 
|  | 108 | // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so | 
|  | 109 | // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable | 
|  | 110 | // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it) | 
|  | 111 | // and by passing it as a parameter we don't need to duplicate its value | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 112 | usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 113 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 114 |  | 
|  | 115 | manifest, err := readManifest(manifestFile) | 
|  | 116 |  | 
|  | 117 | if len(manifest.Commands) == 0 { | 
|  | 118 | return fmt.Errorf("at least one commands entry is required in %q", manifestFile) | 
| Jeff Gaston | 193f2fb | 2017-06-12 15:00:12 -0700 | [diff] [blame] | 119 | } | 
|  | 120 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 121 | // setup sandbox directory | 
|  | 122 | err = os.MkdirAll(sandboxesRoot, 0777) | 
| Jeff Gaston | 8a88db5 | 2017-11-06 13:33:14 -0800 | [diff] [blame] | 123 | if err != nil { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 124 | return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err) | 
| Jeff Gaston | 8a88db5 | 2017-11-06 13:33:14 -0800 | [diff] [blame] | 125 | } | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 126 |  | 
| Ulf Adams | b73daa5 | 2020-11-25 23:09:09 +0100 | [diff] [blame] | 127 | // This tool assumes that there are no two concurrent runs with the same | 
|  | 128 | // manifestFile. It should therefore be safe to use the hash of the | 
|  | 129 | // manifestFile as the temporary directory name. We do this because it | 
|  | 130 | // makes the temporary directory name deterministic. There are some | 
|  | 131 | // tools that embed the name of the temporary output in the output, and | 
|  | 132 | // they otherwise cause non-determinism, which then poisons actions | 
|  | 133 | // depending on this one. | 
|  | 134 | hash := sha1.New() | 
|  | 135 | hash.Write([]byte(manifestFile)) | 
|  | 136 | tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil))) | 
|  | 137 |  | 
|  | 138 | err = os.RemoveAll(tempDir) | 
|  | 139 | if err != nil { | 
|  | 140 | return err | 
|  | 141 | } | 
|  | 142 | err = os.MkdirAll(tempDir, 0777) | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 143 | if err != nil { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 144 | return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err) | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 145 | } | 
|  | 146 |  | 
|  | 147 | // In the common case, the following line of code is what removes the sandbox | 
|  | 148 | // If a fatal error occurs (such as if our Go process is killed unexpectedly), | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 149 | // then at the beginning of the next build, Soong will wipe the temporary | 
|  | 150 | // directory. | 
| Jeff Gaston | f49082a | 2017-06-07 13:22:22 -0700 | [diff] [blame] | 151 | defer func() { | 
|  | 152 | // in some cases we decline to remove the temp dir, to facilitate debugging | 
| Jeff Gaston | 93f0f37 | 2017-11-01 13:33:02 -0700 | [diff] [blame] | 153 | if !keepOutDir { | 
| Jeff Gaston | f49082a | 2017-06-07 13:22:22 -0700 | [diff] [blame] | 154 | os.RemoveAll(tempDir) | 
|  | 155 | } | 
|  | 156 | }() | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 157 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 158 | // If there is more than one command in the manifest use a separate directory for each one. | 
|  | 159 | useSubDir := len(manifest.Commands) > 1 | 
|  | 160 | var commandDepFiles []string | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 161 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 162 | for i, command := range manifest.Commands { | 
|  | 163 | localTempDir := tempDir | 
|  | 164 | if useSubDir { | 
|  | 165 | localTempDir = filepath.Join(localTempDir, strconv.Itoa(i)) | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 166 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 167 | depFile, err := runCommand(command, localTempDir) | 
| Jeff Gaston | 02a684b | 2017-10-27 14:59:27 -0700 | [diff] [blame] | 168 | if err != nil { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 169 | // Running the command failed, keep the temporary output directory around in | 
|  | 170 | // case a user wants to inspect it for debugging purposes.  Soong will delete | 
|  | 171 | // it at the beginning of the next build anyway. | 
|  | 172 | keepOutDir = true | 
| Jeff Gaston | 02a684b | 2017-10-27 14:59:27 -0700 | [diff] [blame] | 173 | return err | 
|  | 174 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 175 | if depFile != "" { | 
|  | 176 | commandDepFiles = append(commandDepFiles, depFile) | 
|  | 177 | } | 
|  | 178 | } | 
|  | 179 |  | 
|  | 180 | outputDepFile := manifest.GetOutputDepfile() | 
|  | 181 | if len(commandDepFiles) > 0 && outputDepFile == "" { | 
|  | 182 | return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file", | 
|  | 183 | depFilePlaceholder) | 
|  | 184 | } | 
|  | 185 |  | 
|  | 186 | if outputDepFile != "" { | 
|  | 187 | // Merge the depfiles from each command in the manifest to a single output depfile. | 
|  | 188 | err = rewriteDepFiles(commandDepFiles, outputDepFile) | 
|  | 189 | if err != nil { | 
|  | 190 | return fmt.Errorf("failed merging depfiles: %w", err) | 
|  | 191 | } | 
|  | 192 | } | 
|  | 193 |  | 
|  | 194 | return nil | 
|  | 195 | } | 
|  | 196 |  | 
|  | 197 | // readManifest reads an sbox manifest from a textproto file. | 
|  | 198 | func readManifest(file string) (*sbox_proto.Manifest, error) { | 
|  | 199 | manifestData, err := ioutil.ReadFile(file) | 
|  | 200 | if err != nil { | 
|  | 201 | return nil, fmt.Errorf("error reading manifest %q: %w", file, err) | 
|  | 202 | } | 
|  | 203 |  | 
|  | 204 | manifest := sbox_proto.Manifest{} | 
|  | 205 |  | 
|  | 206 | err = proto.UnmarshalText(string(manifestData), &manifest) | 
|  | 207 | if err != nil { | 
|  | 208 | return nil, fmt.Errorf("error parsing manifest %q: %w", file, err) | 
|  | 209 | } | 
|  | 210 |  | 
|  | 211 | return &manifest, nil | 
|  | 212 | } | 
|  | 213 |  | 
|  | 214 | // runCommand runs a single command from a manifest.  If the command references the | 
|  | 215 | // __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used. | 
|  | 216 | func runCommand(command *sbox_proto.Command, tempDir string) (depFile string, err error) { | 
|  | 217 | rawCommand := command.GetCommand() | 
|  | 218 | if rawCommand == "" { | 
|  | 219 | return "", fmt.Errorf("command is required") | 
|  | 220 | } | 
|  | 221 |  | 
| Colin Cross | e55bd42 | 2021-03-23 13:44:30 -0700 | [diff] [blame] | 222 | pathToTempDirInSbox := tempDir | 
|  | 223 | if command.GetChdir() { | 
|  | 224 | pathToTempDirInSbox = "." | 
|  | 225 | } | 
|  | 226 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 227 | err = os.MkdirAll(tempDir, 0777) | 
|  | 228 | if err != nil { | 
|  | 229 | return "", fmt.Errorf("failed to create %q: %w", tempDir, err) | 
|  | 230 | } | 
|  | 231 |  | 
|  | 232 | // Copy in any files specified by the manifest. | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 233 | err = copyFiles(command.CopyBefore, "", tempDir, false) | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 234 | if err != nil { | 
|  | 235 | return "", err | 
|  | 236 | } | 
| Colin Cross | e55bd42 | 2021-03-23 13:44:30 -0700 | [diff] [blame] | 237 | err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox) | 
|  | 238 | if err != nil { | 
|  | 239 | return "", err | 
| Colin Cross | c590ec4 | 2021-03-11 17:20:02 -0800 | [diff] [blame] | 240 | } | 
|  | 241 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 242 | if strings.Contains(rawCommand, depFilePlaceholder) { | 
| Colin Cross | c590ec4 | 2021-03-11 17:20:02 -0800 | [diff] [blame] | 243 | depFile = filepath.Join(pathToTempDirInSbox, "deps.d") | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 244 | rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1) | 
|  | 245 | } | 
|  | 246 |  | 
|  | 247 | if strings.Contains(rawCommand, sandboxDirPlaceholder) { | 
| Colin Cross | c590ec4 | 2021-03-11 17:20:02 -0800 | [diff] [blame] | 248 | rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1) | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 249 | } | 
|  | 250 |  | 
|  | 251 | // Emulate ninja's behavior of creating the directories for any output files before | 
|  | 252 | // running the command. | 
|  | 253 | err = makeOutputDirs(command.CopyAfter, tempDir) | 
|  | 254 | if err != nil { | 
|  | 255 | return "", err | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 256 | } | 
|  | 257 |  | 
|  | 258 | cmd := exec.Command("bash", "-c", rawCommand) | 
| Colin Cross | 4258a39 | 2021-04-15 18:50:11 -0700 | [diff] [blame] | 259 | buf := &bytes.Buffer{} | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 260 | cmd.Stdin = os.Stdin | 
| Colin Cross | 4258a39 | 2021-04-15 18:50:11 -0700 | [diff] [blame] | 261 | cmd.Stdout = buf | 
|  | 262 | cmd.Stderr = buf | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 263 |  | 
|  | 264 | if command.GetChdir() { | 
|  | 265 | cmd.Dir = tempDir | 
| Colin Cross | c590ec4 | 2021-03-11 17:20:02 -0800 | [diff] [blame] | 266 | path := os.Getenv("PATH") | 
|  | 267 | absPath, err := makeAbsPathEnv(path) | 
|  | 268 | if err != nil { | 
|  | 269 | return "", err | 
|  | 270 | } | 
|  | 271 | err = os.Setenv("PATH", absPath) | 
|  | 272 | if err != nil { | 
|  | 273 | return "", fmt.Errorf("Failed to update PATH: %w", err) | 
|  | 274 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 275 | } | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 276 | err = cmd.Run() | 
| Jeff Gaston | 193f2fb | 2017-06-12 15:00:12 -0700 | [diff] [blame] | 277 |  | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 278 | if err != nil { | 
|  | 279 | // The command failed, do a best effort copy of output files out of the sandbox.  This is | 
|  | 280 | // especially useful for linters with baselines that print an error message on failure | 
|  | 281 | // with a command to copy the output lint errors to the new baseline.  Use a copy instead of | 
|  | 282 | // a move to leave the sandbox intact for manual inspection | 
|  | 283 | copyFiles(command.CopyAfter, tempDir, "", true) | 
|  | 284 | } | 
|  | 285 |  | 
| Colin Cross | 4258a39 | 2021-04-15 18:50:11 -0700 | [diff] [blame] | 286 | // If the command  was executed but failed with an error, print a debugging message before | 
|  | 287 | // the command's output so it doesn't scroll the real error message off the screen. | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 288 | if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { | 
| Colin Cross | 4258a39 | 2021-04-15 18:50:11 -0700 | [diff] [blame] | 289 | fmt.Fprintf(os.Stderr, | 
|  | 290 | "The failing command was run inside an sbox sandbox in temporary directory\n"+ | 
|  | 291 | "%s\n"+ | 
|  | 292 | "The failing command line was:\n"+ | 
|  | 293 | "%s\n", | 
|  | 294 | tempDir, rawCommand) | 
|  | 295 | } | 
|  | 296 |  | 
|  | 297 | // Write the command's combined stdout/stderr. | 
|  | 298 | os.Stdout.Write(buf.Bytes()) | 
|  | 299 |  | 
|  | 300 | if err != nil { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 301 | return "", err | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 302 | } | 
|  | 303 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 304 | missingOutputErrors := validateOutputFiles(command.CopyAfter, tempDir) | 
|  | 305 |  | 
| Jeff Gaston | 90cfb09 | 2017-09-26 16:46:10 -0700 | [diff] [blame] | 306 | if len(missingOutputErrors) > 0 { | 
|  | 307 | // find all created files for making a more informative error message | 
|  | 308 | createdFiles := findAllFilesUnder(tempDir) | 
|  | 309 |  | 
|  | 310 | // build error message | 
|  | 311 | errorMessage := "mismatch between declared and actual outputs\n" | 
| Colin Cross | 4258a39 | 2021-04-15 18:50:11 -0700 | [diff] [blame] | 312 | errorMessage += "in sbox command(" + rawCommand + ")\n\n" | 
| Jeff Gaston | 90cfb09 | 2017-09-26 16:46:10 -0700 | [diff] [blame] | 313 | errorMessage += "in sandbox " + tempDir + ",\n" | 
|  | 314 | errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors)) | 
|  | 315 | for _, missingOutputError := range missingOutputErrors { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 316 | errorMessage += "  " + missingOutputError.Error() + "\n" | 
| Jeff Gaston | 90cfb09 | 2017-09-26 16:46:10 -0700 | [diff] [blame] | 317 | } | 
|  | 318 | if len(createdFiles) < 1 { | 
|  | 319 | errorMessage += "created 0 files." | 
|  | 320 | } else { | 
|  | 321 | errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles)) | 
|  | 322 | creationMessages := createdFiles | 
|  | 323 | maxNumCreationLines := 10 | 
|  | 324 | if len(creationMessages) > maxNumCreationLines { | 
|  | 325 | creationMessages = creationMessages[:maxNumCreationLines] | 
|  | 326 | creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines)) | 
|  | 327 | } | 
|  | 328 | for _, creationMessage := range creationMessages { | 
|  | 329 | errorMessage += "  " + creationMessage + "\n" | 
|  | 330 | } | 
|  | 331 | } | 
|  | 332 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 333 | return "", errors.New(errorMessage) | 
| Jeff Gaston | f49082a | 2017-06-07 13:22:22 -0700 | [diff] [blame] | 334 | } | 
|  | 335 | // the created files match the declared files; now move them | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 336 | err = moveFiles(command.CopyAfter, tempDir, "") | 
|  | 337 |  | 
|  | 338 | return depFile, nil | 
|  | 339 | } | 
|  | 340 |  | 
|  | 341 | // makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied | 
|  | 342 | // out of the sandbox.  This emulate's Ninja's behavior of creating directories for output files | 
|  | 343 | // so that the tools don't have to. | 
|  | 344 | func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error { | 
|  | 345 | for _, copyPair := range copies { | 
|  | 346 | dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom())) | 
|  | 347 | err := os.MkdirAll(dir, 0777) | 
|  | 348 | if err != nil { | 
|  | 349 | return err | 
| Jeff Gaston | 193f2fb | 2017-06-12 15:00:12 -0700 | [diff] [blame] | 350 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 351 | } | 
|  | 352 | return nil | 
|  | 353 | } | 
|  | 354 |  | 
|  | 355 | // validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox | 
|  | 356 | // were created by the command. | 
|  | 357 | func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir string) []error { | 
|  | 358 | var missingOutputErrors []error | 
|  | 359 | for _, copyPair := range copies { | 
|  | 360 | fromPath := joinPath(sandboxDir, copyPair.GetFrom()) | 
|  | 361 | fileInfo, err := os.Stat(fromPath) | 
|  | 362 | if err != nil { | 
|  | 363 | missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath)) | 
|  | 364 | continue | 
|  | 365 | } | 
|  | 366 | if fileInfo.IsDir() { | 
|  | 367 | missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath)) | 
|  | 368 | } | 
|  | 369 | } | 
|  | 370 | return missingOutputErrors | 
|  | 371 | } | 
|  | 372 |  | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 373 | // copyFiles copies files in or out of the sandbox.  If allowFromNotExists is true then errors | 
|  | 374 | // caused by a from path not existing are ignored. | 
|  | 375 | func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, allowFromNotExists bool) error { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 376 | for _, copyPair := range copies { | 
|  | 377 | fromPath := joinPath(fromDir, copyPair.GetFrom()) | 
|  | 378 | toPath := joinPath(toDir, copyPair.GetTo()) | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 379 | err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), allowFromNotExists) | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 380 | if err != nil { | 
|  | 381 | return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err) | 
|  | 382 | } | 
|  | 383 | } | 
|  | 384 | return nil | 
|  | 385 | } | 
|  | 386 |  | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 387 | // copyOneFile copies a file and its permissions.  If forceExecutable is true it adds u+x to the | 
|  | 388 | // permissions.  If allowFromNotExists is true it returns nil if the from path doesn't exist. | 
|  | 389 | func copyOneFile(from string, to string, forceExecutable, allowFromNotExists bool) error { | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 390 | err := os.MkdirAll(filepath.Dir(to), 0777) | 
|  | 391 | if err != nil { | 
|  | 392 | return err | 
|  | 393 | } | 
|  | 394 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 395 | stat, err := os.Stat(from) | 
|  | 396 | if err != nil { | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 397 | if os.IsNotExist(err) && allowFromNotExists { | 
|  | 398 | return nil | 
|  | 399 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 400 | return err | 
|  | 401 | } | 
|  | 402 |  | 
|  | 403 | perm := stat.Mode() | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 404 | if forceExecutable { | 
| Colin Cross | 859dfd9 | 2020-11-30 20:12:47 -0800 | [diff] [blame] | 405 | perm = perm | 0100 // u+x | 
|  | 406 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 407 |  | 
|  | 408 | in, err := os.Open(from) | 
|  | 409 | if err != nil { | 
|  | 410 | return err | 
|  | 411 | } | 
|  | 412 | defer in.Close() | 
|  | 413 |  | 
| Colin Cross | 607c0b7 | 2021-03-31 12:54:06 -0700 | [diff] [blame] | 414 | // Remove the target before copying.  In most cases the file won't exist, but if there are | 
|  | 415 | // duplicate copy rules for a file and the source file was read-only the second copy could | 
|  | 416 | // fail. | 
|  | 417 | err = os.Remove(to) | 
|  | 418 | if err != nil && !os.IsNotExist(err) { | 
|  | 419 | return err | 
|  | 420 | } | 
|  | 421 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 422 | out, err := os.Create(to) | 
|  | 423 | if err != nil { | 
|  | 424 | return err | 
|  | 425 | } | 
|  | 426 | defer func() { | 
|  | 427 | out.Close() | 
|  | 428 | if err != nil { | 
|  | 429 | os.Remove(to) | 
|  | 430 | } | 
|  | 431 | }() | 
|  | 432 |  | 
|  | 433 | _, err = io.Copy(out, in) | 
|  | 434 | if err != nil { | 
|  | 435 | return err | 
|  | 436 | } | 
|  | 437 |  | 
|  | 438 | if err = out.Close(); err != nil { | 
|  | 439 | return err | 
|  | 440 | } | 
|  | 441 |  | 
|  | 442 | if err = os.Chmod(to, perm); err != nil { | 
|  | 443 | return err | 
|  | 444 | } | 
|  | 445 |  | 
|  | 446 | return nil | 
|  | 447 | } | 
|  | 448 |  | 
| Colin Cross | e55bd42 | 2021-03-23 13:44:30 -0700 | [diff] [blame] | 449 | // copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files | 
|  | 450 | // listed into the sandbox. | 
|  | 451 | func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error { | 
|  | 452 | for _, rspFile := range rspFiles { | 
|  | 453 | err := copyOneRspFile(rspFile, toDir, toDirInSandbox) | 
|  | 454 | if err != nil { | 
|  | 455 | return err | 
|  | 456 | } | 
|  | 457 | } | 
|  | 458 | return nil | 
|  | 459 | } | 
|  | 460 |  | 
|  | 461 | // copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files | 
|  | 462 | // listed into the sandbox. | 
|  | 463 | func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error { | 
|  | 464 | in, err := os.Open(rspFile.GetFile()) | 
|  | 465 | if err != nil { | 
|  | 466 | return err | 
|  | 467 | } | 
|  | 468 | defer in.Close() | 
|  | 469 |  | 
|  | 470 | files, err := response.ReadRspFile(in) | 
|  | 471 | if err != nil { | 
|  | 472 | return err | 
|  | 473 | } | 
|  | 474 |  | 
|  | 475 | for i, from := range files { | 
|  | 476 | // Convert the real path of the input file into the path inside the sandbox using the | 
|  | 477 | // path mappings. | 
|  | 478 | to := applyPathMappings(rspFile.PathMappings, from) | 
|  | 479 |  | 
|  | 480 | // Copy the file into the sandbox. | 
| Colin Cross | fc2d842 | 2021-04-15 18:36:42 -0700 | [diff] [blame] | 481 | err := copyOneFile(from, joinPath(toDir, to), false, false) | 
| Colin Cross | e55bd42 | 2021-03-23 13:44:30 -0700 | [diff] [blame] | 482 | if err != nil { | 
|  | 483 | return err | 
|  | 484 | } | 
|  | 485 |  | 
|  | 486 | // Rewrite the name in the list of files to be relative to the sandbox directory. | 
|  | 487 | files[i] = joinPath(toDirInSandbox, to) | 
|  | 488 | } | 
|  | 489 |  | 
|  | 490 | // Convert the real path of the rsp file into the path inside the sandbox using the path | 
|  | 491 | // mappings. | 
|  | 492 | outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile())) | 
|  | 493 |  | 
|  | 494 | err = os.MkdirAll(filepath.Dir(outRspFile), 0777) | 
|  | 495 | if err != nil { | 
|  | 496 | return err | 
|  | 497 | } | 
|  | 498 |  | 
|  | 499 | out, err := os.Create(outRspFile) | 
|  | 500 | if err != nil { | 
|  | 501 | return err | 
|  | 502 | } | 
|  | 503 | defer out.Close() | 
|  | 504 |  | 
|  | 505 | // Write the rsp file with converted paths into the sandbox. | 
|  | 506 | err = response.WriteRspFile(out, files) | 
|  | 507 | if err != nil { | 
|  | 508 | return err | 
|  | 509 | } | 
|  | 510 |  | 
|  | 511 | return nil | 
|  | 512 | } | 
|  | 513 |  | 
|  | 514 | // applyPathMappings takes a list of path mappings and a path, and returns the path with the first | 
|  | 515 | // matching path mapping applied.  If the path does not match any of the path mappings then it is | 
|  | 516 | // returned unmodified. | 
|  | 517 | func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string { | 
|  | 518 | for _, mapping := range pathMappings { | 
|  | 519 | if strings.HasPrefix(path, mapping.GetFrom()+"/") { | 
|  | 520 | return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/")) | 
|  | 521 | } | 
|  | 522 | } | 
|  | 523 | return path | 
|  | 524 | } | 
|  | 525 |  | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 526 | // moveFiles moves files specified by a set of copy rules.  It uses os.Rename, so it is restricted | 
|  | 527 | // to moving files where the source and destination are in the same filesystem.  This is OK for | 
|  | 528 | // sbox because the temporary directory is inside the out directory.  It updates the timestamp | 
|  | 529 | // of the new file. | 
|  | 530 | func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error { | 
|  | 531 | for _, copyPair := range copies { | 
|  | 532 | fromPath := joinPath(fromDir, copyPair.GetFrom()) | 
|  | 533 | toPath := joinPath(toDir, copyPair.GetTo()) | 
|  | 534 | err := os.MkdirAll(filepath.Dir(toPath), 0777) | 
|  | 535 | if err != nil { | 
|  | 536 | return err | 
|  | 537 | } | 
|  | 538 |  | 
|  | 539 | err = os.Rename(fromPath, toPath) | 
| Jeff Gaston | 8a88db5 | 2017-11-06 13:33:14 -0800 | [diff] [blame] | 540 | if err != nil { | 
|  | 541 | return err | 
|  | 542 | } | 
| Colin Cross | d1c1e6f | 2019-03-29 13:54:39 -0700 | [diff] [blame] | 543 |  | 
|  | 544 | // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract | 
|  | 545 | // files with old timestamps). | 
|  | 546 | now := time.Now() | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 547 | err = os.Chtimes(toPath, now, now) | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 548 | if err != nil { | 
|  | 549 | return err | 
|  | 550 | } | 
|  | 551 | } | 
| Jeff Gaston | efc1b41 | 2017-03-29 17:29:06 -0700 | [diff] [blame] | 552 | return nil | 
|  | 553 | } | 
| Colin Cross | e16ce36 | 2020-11-12 08:29:30 -0800 | [diff] [blame] | 554 |  | 
|  | 555 | // Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory | 
|  | 556 | // to an output file. | 
|  | 557 | func rewriteDepFiles(ins []string, out string) error { | 
|  | 558 | var mergedDeps []string | 
|  | 559 | for _, in := range ins { | 
|  | 560 | data, err := ioutil.ReadFile(in) | 
|  | 561 | if err != nil { | 
|  | 562 | return err | 
|  | 563 | } | 
|  | 564 |  | 
|  | 565 | deps, err := makedeps.Parse(in, bytes.NewBuffer(data)) | 
|  | 566 | if err != nil { | 
|  | 567 | return err | 
|  | 568 | } | 
|  | 569 | mergedDeps = append(mergedDeps, deps.Inputs...) | 
|  | 570 | } | 
|  | 571 |  | 
|  | 572 | deps := makedeps.Deps{ | 
|  | 573 | // Ninja doesn't care what the output file is, so we can use any string here. | 
|  | 574 | Output: "outputfile", | 
|  | 575 | Inputs: mergedDeps, | 
|  | 576 | } | 
|  | 577 |  | 
|  | 578 | // Make the directory for the output depfile in case it is in a different directory | 
|  | 579 | // than any of the output files. | 
|  | 580 | outDir := filepath.Dir(out) | 
|  | 581 | err := os.MkdirAll(outDir, 0777) | 
|  | 582 | if err != nil { | 
|  | 583 | return fmt.Errorf("failed to create %q: %w", outDir, err) | 
|  | 584 | } | 
|  | 585 |  | 
|  | 586 | return ioutil.WriteFile(out, deps.Print(), 0666) | 
|  | 587 | } | 
|  | 588 |  | 
|  | 589 | // joinPath wraps filepath.Join but returns file without appending to dir if file is | 
|  | 590 | // absolute. | 
|  | 591 | func joinPath(dir, file string) string { | 
|  | 592 | if filepath.IsAbs(file) { | 
|  | 593 | return file | 
|  | 594 | } | 
|  | 595 | return filepath.Join(dir, file) | 
|  | 596 | } | 
| Colin Cross | c590ec4 | 2021-03-11 17:20:02 -0800 | [diff] [blame] | 597 |  | 
|  | 598 | func makeAbsPathEnv(pathEnv string) (string, error) { | 
|  | 599 | pathEnvElements := filepath.SplitList(pathEnv) | 
|  | 600 | for i, p := range pathEnvElements { | 
|  | 601 | if !filepath.IsAbs(p) { | 
|  | 602 | absPath, err := filepath.Abs(p) | 
|  | 603 | if err != nil { | 
|  | 604 | return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err) | 
|  | 605 | } | 
|  | 606 | pathEnvElements[i] = absPath | 
|  | 607 | } | 
|  | 608 | } | 
|  | 609 | return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil | 
|  | 610 | } |