blob: f8919a4007fc2e1f585b0c2e0e090dad4ae027d4 [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"
Ulf Adamsb73daa52020-11-25 23:09:09 +010019 "crypto/sha1"
20 "encoding/hex"
Jeff Gaston90cfb092017-09-26 16:46:10 -070021 "errors"
Jeff Gaston93f0f372017-11-01 13:33:02 -070022 "flag"
Jeff Gastonefc1b412017-03-29 17:29:06 -070023 "fmt"
Colin Crosse16ce362020-11-12 08:29:30 -080024 "io"
Jeff Gastonefc1b412017-03-29 17:29:06 -070025 "io/ioutil"
26 "os"
27 "os/exec"
Jeff Gastonefc1b412017-03-29 17:29:06 -070028 "path/filepath"
Colin Crosse16ce362020-11-12 08:29:30 -080029 "strconv"
Jeff Gastonefc1b412017-03-29 17:29:06 -070030 "strings"
Colin Crossd1c1e6f2019-03-29 13:54:39 -070031 "time"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070032
Colin Crosse16ce362020-11-12 08:29:30 -080033 "android/soong/cmd/sbox/sbox_proto"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070034 "android/soong/makedeps"
Colin Crosse16ce362020-11-12 08:29:30 -080035
36 "github.com/golang/protobuf/proto"
Jeff Gastonefc1b412017-03-29 17:29:06 -070037)
38
Jeff Gaston93f0f372017-11-01 13:33:02 -070039var (
40 sandboxesRoot string
Colin Crosse16ce362020-11-12 08:29:30 -080041 manifestFile string
Jeff Gaston93f0f372017-11-01 13:33:02 -070042 keepOutDir bool
Colin Crosse16ce362020-11-12 08:29:30 -080043)
44
45const (
46 depFilePlaceholder = "__SBOX_DEPFILE__"
47 sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__"
Jeff Gaston93f0f372017-11-01 13:33:02 -070048)
49
50func init() {
51 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
52 "root of temp directory to put the sandbox into")
Colin Crosse16ce362020-11-12 08:29:30 -080053 flag.StringVar(&manifestFile, "manifest", "",
54 "textproto manifest describing the sandboxed command(s)")
Jeff Gaston93f0f372017-11-01 13:33:02 -070055 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
56 "whether to keep the sandbox directory when done")
Jeff Gaston93f0f372017-11-01 13:33:02 -070057}
58
59func usageViolation(violation string) {
60 if violation != "" {
61 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
62 }
63
64 fmt.Fprintf(os.Stderr,
Colin Crosse16ce362020-11-12 08:29:30 -080065 "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070066
67 flag.PrintDefaults()
68
69 os.Exit(1)
70}
71
Jeff Gastonefc1b412017-03-29 17:29:06 -070072func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070073 flag.Usage = func() {
74 usageViolation("")
75 }
76 flag.Parse()
77
Jeff Gastonefc1b412017-03-29 17:29:06 -070078 error := run()
79 if error != nil {
80 fmt.Fprintln(os.Stderr, error)
81 os.Exit(1)
82 }
83}
84
Jeff Gaston90cfb092017-09-26 16:46:10 -070085func findAllFilesUnder(root string) (paths []string) {
86 paths = []string{}
87 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
88 if !info.IsDir() {
89 relPath, err := filepath.Rel(root, path)
90 if err != nil {
91 // couldn't find relative path from ancestor?
92 panic(err)
93 }
94 paths = append(paths, relPath)
95 }
96 return nil
97 })
98 return paths
99}
100
Jeff Gastonefc1b412017-03-29 17:29:06 -0700101func run() error {
Colin Crosse16ce362020-11-12 08:29:30 -0800102 if manifestFile == "" {
103 usageViolation("--manifest <manifest> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700104 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700105 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700106 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
107 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
108 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
109 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
110 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700111 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700112 }
Colin Crosse16ce362020-11-12 08:29:30 -0800113
114 manifest, err := readManifest(manifestFile)
115
116 if len(manifest.Commands) == 0 {
117 return fmt.Errorf("at least one commands entry is required in %q", manifestFile)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700118 }
119
Colin Crosse16ce362020-11-12 08:29:30 -0800120 // setup sandbox directory
121 err = os.MkdirAll(sandboxesRoot, 0777)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800122 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800123 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800124 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700125
Ulf Adamsb73daa52020-11-25 23:09:09 +0100126 // This tool assumes that there are no two concurrent runs with the same
127 // manifestFile. It should therefore be safe to use the hash of the
128 // manifestFile as the temporary directory name. We do this because it
129 // makes the temporary directory name deterministic. There are some
130 // tools that embed the name of the temporary output in the output, and
131 // they otherwise cause non-determinism, which then poisons actions
132 // depending on this one.
133 hash := sha1.New()
134 hash.Write([]byte(manifestFile))
135 tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil)))
136
137 err = os.RemoveAll(tempDir)
138 if err != nil {
139 return err
140 }
141 err = os.MkdirAll(tempDir, 0777)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700142 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800143 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700144 }
145
146 // In the common case, the following line of code is what removes the sandbox
147 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
Colin Crosse16ce362020-11-12 08:29:30 -0800148 // then at the beginning of the next build, Soong will wipe the temporary
149 // directory.
Jeff Gastonf49082a2017-06-07 13:22:22 -0700150 defer func() {
151 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700152 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700153 os.RemoveAll(tempDir)
154 }
155 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700156
Colin Crosse16ce362020-11-12 08:29:30 -0800157 // If there is more than one command in the manifest use a separate directory for each one.
158 useSubDir := len(manifest.Commands) > 1
159 var commandDepFiles []string
Jeff Gastonefc1b412017-03-29 17:29:06 -0700160
Colin Crosse16ce362020-11-12 08:29:30 -0800161 for i, command := range manifest.Commands {
162 localTempDir := tempDir
163 if useSubDir {
164 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700165 }
Colin Crosse16ce362020-11-12 08:29:30 -0800166 depFile, err := runCommand(command, localTempDir)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700167 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800168 // Running the command failed, keep the temporary output directory around in
169 // case a user wants to inspect it for debugging purposes. Soong will delete
170 // it at the beginning of the next build anyway.
171 keepOutDir = true
Jeff Gaston02a684b2017-10-27 14:59:27 -0700172 return err
173 }
Colin Crosse16ce362020-11-12 08:29:30 -0800174 if depFile != "" {
175 commandDepFiles = append(commandDepFiles, depFile)
176 }
177 }
178
179 outputDepFile := manifest.GetOutputDepfile()
180 if len(commandDepFiles) > 0 && outputDepFile == "" {
181 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file",
182 depFilePlaceholder)
183 }
184
185 if outputDepFile != "" {
186 // Merge the depfiles from each command in the manifest to a single output depfile.
187 err = rewriteDepFiles(commandDepFiles, outputDepFile)
188 if err != nil {
189 return fmt.Errorf("failed merging depfiles: %w", err)
190 }
191 }
192
193 return nil
194}
195
196// readManifest reads an sbox manifest from a textproto file.
197func readManifest(file string) (*sbox_proto.Manifest, error) {
198 manifestData, err := ioutil.ReadFile(file)
199 if err != nil {
200 return nil, fmt.Errorf("error reading manifest %q: %w", file, err)
201 }
202
203 manifest := sbox_proto.Manifest{}
204
205 err = proto.UnmarshalText(string(manifestData), &manifest)
206 if err != nil {
207 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err)
208 }
209
210 return &manifest, nil
211}
212
213// runCommand runs a single command from a manifest. If the command references the
214// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
215func runCommand(command *sbox_proto.Command, tempDir string) (depFile string, err error) {
216 rawCommand := command.GetCommand()
217 if rawCommand == "" {
218 return "", fmt.Errorf("command is required")
219 }
220
221 err = os.MkdirAll(tempDir, 0777)
222 if err != nil {
223 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
224 }
225
226 // Copy in any files specified by the manifest.
Colin Crossd03797e2020-11-25 10:24:51 -0800227 err = copyFiles(command.CopyBefore, "", tempDir)
Colin Crosse16ce362020-11-12 08:29:30 -0800228 if err != nil {
229 return "", err
230 }
231
232 if strings.Contains(rawCommand, depFilePlaceholder) {
233 depFile = filepath.Join(tempDir, "deps.d")
234 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
235 }
236
237 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
238 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, tempDir, -1)
239 }
240
241 // Emulate ninja's behavior of creating the directories for any output files before
242 // running the command.
243 err = makeOutputDirs(command.CopyAfter, tempDir)
244 if err != nil {
245 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700246 }
247
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700248 commandDescription := rawCommand
249
Jeff Gastonefc1b412017-03-29 17:29:06 -0700250 cmd := exec.Command("bash", "-c", rawCommand)
251 cmd.Stdin = os.Stdin
252 cmd.Stdout = os.Stdout
253 cmd.Stderr = os.Stderr
Colin Crosse16ce362020-11-12 08:29:30 -0800254
255 if command.GetChdir() {
256 cmd.Dir = tempDir
257 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700258 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700259
Jeff Gastonefc1b412017-03-29 17:29:06 -0700260 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Crosse16ce362020-11-12 08:29:30 -0800261 return "", fmt.Errorf("sbox command failed with err:\n%s\n%w\n", commandDescription, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700262 } else if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800263 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700264 }
265
Colin Crosse16ce362020-11-12 08:29:30 -0800266 missingOutputErrors := validateOutputFiles(command.CopyAfter, tempDir)
267
Jeff Gaston90cfb092017-09-26 16:46:10 -0700268 if len(missingOutputErrors) > 0 {
269 // find all created files for making a more informative error message
270 createdFiles := findAllFilesUnder(tempDir)
271
272 // build error message
273 errorMessage := "mismatch between declared and actual outputs\n"
274 errorMessage += "in sbox command(" + commandDescription + ")\n\n"
275 errorMessage += "in sandbox " + tempDir + ",\n"
276 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
277 for _, missingOutputError := range missingOutputErrors {
Colin Crosse16ce362020-11-12 08:29:30 -0800278 errorMessage += " " + missingOutputError.Error() + "\n"
Jeff Gaston90cfb092017-09-26 16:46:10 -0700279 }
280 if len(createdFiles) < 1 {
281 errorMessage += "created 0 files."
282 } else {
283 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
284 creationMessages := createdFiles
285 maxNumCreationLines := 10
286 if len(creationMessages) > maxNumCreationLines {
287 creationMessages = creationMessages[:maxNumCreationLines]
288 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines))
289 }
290 for _, creationMessage := range creationMessages {
291 errorMessage += " " + creationMessage + "\n"
292 }
293 }
294
Colin Crosse16ce362020-11-12 08:29:30 -0800295 return "", errors.New(errorMessage)
Jeff Gastonf49082a2017-06-07 13:22:22 -0700296 }
297 // the created files match the declared files; now move them
Colin Crosse16ce362020-11-12 08:29:30 -0800298 err = moveFiles(command.CopyAfter, tempDir, "")
299
300 return depFile, nil
301}
302
303// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
304// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
305// so that the tools don't have to.
306func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
307 for _, copyPair := range copies {
308 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
309 err := os.MkdirAll(dir, 0777)
310 if err != nil {
311 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700312 }
Colin Crosse16ce362020-11-12 08:29:30 -0800313 }
314 return nil
315}
316
317// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
318// were created by the command.
319func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir string) []error {
320 var missingOutputErrors []error
321 for _, copyPair := range copies {
322 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
323 fileInfo, err := os.Stat(fromPath)
324 if err != nil {
325 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
326 continue
327 }
328 if fileInfo.IsDir() {
329 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
330 }
331 }
332 return missingOutputErrors
333}
334
Colin Crossd03797e2020-11-25 10:24:51 -0800335// copyFiles copies files in or out of the sandbox.
336func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800337 for _, copyPair := range copies {
338 fromPath := joinPath(fromDir, copyPair.GetFrom())
339 toPath := joinPath(toDir, copyPair.GetTo())
Colin Cross859dfd92020-11-30 20:12:47 -0800340 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable())
Colin Crosse16ce362020-11-12 08:29:30 -0800341 if err != nil {
342 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
343 }
344 }
345 return nil
346}
347
Colin Crossd03797e2020-11-25 10:24:51 -0800348// copyOneFile copies a file.
Colin Cross859dfd92020-11-30 20:12:47 -0800349func copyOneFile(from string, to string, executable bool) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800350 err := os.MkdirAll(filepath.Dir(to), 0777)
351 if err != nil {
352 return err
353 }
354
Colin Crosse16ce362020-11-12 08:29:30 -0800355 stat, err := os.Stat(from)
356 if err != nil {
357 return err
358 }
359
360 perm := stat.Mode()
Colin Cross859dfd92020-11-30 20:12:47 -0800361 if executable {
362 perm = perm | 0100 // u+x
363 }
Colin Crosse16ce362020-11-12 08:29:30 -0800364
365 in, err := os.Open(from)
366 if err != nil {
367 return err
368 }
369 defer in.Close()
370
371 out, err := os.Create(to)
372 if err != nil {
373 return err
374 }
375 defer func() {
376 out.Close()
377 if err != nil {
378 os.Remove(to)
379 }
380 }()
381
382 _, err = io.Copy(out, in)
383 if err != nil {
384 return err
385 }
386
387 if err = out.Close(); err != nil {
388 return err
389 }
390
391 if err = os.Chmod(to, perm); err != nil {
392 return err
393 }
394
395 return nil
396}
397
398// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
399// to moving files where the source and destination are in the same filesystem. This is OK for
400// sbox because the temporary directory is inside the out directory. It updates the timestamp
401// of the new file.
402func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error {
403 for _, copyPair := range copies {
404 fromPath := joinPath(fromDir, copyPair.GetFrom())
405 toPath := joinPath(toDir, copyPair.GetTo())
406 err := os.MkdirAll(filepath.Dir(toPath), 0777)
407 if err != nil {
408 return err
409 }
410
411 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800412 if err != nil {
413 return err
414 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700415
416 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
417 // files with old timestamps).
418 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800419 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700420 if err != nil {
421 return err
422 }
423 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700424 return nil
425}
Colin Crosse16ce362020-11-12 08:29:30 -0800426
427// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
428// to an output file.
429func rewriteDepFiles(ins []string, out string) error {
430 var mergedDeps []string
431 for _, in := range ins {
432 data, err := ioutil.ReadFile(in)
433 if err != nil {
434 return err
435 }
436
437 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
438 if err != nil {
439 return err
440 }
441 mergedDeps = append(mergedDeps, deps.Inputs...)
442 }
443
444 deps := makedeps.Deps{
445 // Ninja doesn't care what the output file is, so we can use any string here.
446 Output: "outputfile",
447 Inputs: mergedDeps,
448 }
449
450 // Make the directory for the output depfile in case it is in a different directory
451 // than any of the output files.
452 outDir := filepath.Dir(out)
453 err := os.MkdirAll(outDir, 0777)
454 if err != nil {
455 return fmt.Errorf("failed to create %q: %w", outDir, err)
456 }
457
458 return ioutil.WriteFile(out, deps.Print(), 0666)
459}
460
461// joinPath wraps filepath.Join but returns file without appending to dir if file is
462// absolute.
463func joinPath(dir, file string) string {
464 if filepath.IsAbs(file) {
465 return file
466 }
467 return filepath.Join(dir, file)
468}