blob: 03ce2d522ed2813e5f1c26477d2c815de4df1542 [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 Crosse55bd422021-03-23 13:44:30 -070035 "android/soong/response"
Colin Crosse16ce362020-11-12 08:29:30 -080036
Dan Willemsen4591b642021-05-24 14:24:12 -070037 "google.golang.org/protobuf/encoding/prototext"
Jeff Gastonefc1b412017-03-29 17:29:06 -070038)
39
Jeff Gaston93f0f372017-11-01 13:33:02 -070040var (
Colin Crosse52c2ac2022-03-28 17:03:35 -070041 sandboxesRoot string
42 outputDir string
43 manifestFile string
44 keepOutDir bool
45 writeIfChanged bool
Colin Crosse16ce362020-11-12 08:29:30 -080046)
47
48const (
49 depFilePlaceholder = "__SBOX_DEPFILE__"
50 sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__"
Jeff Gaston93f0f372017-11-01 13:33:02 -070051)
52
53func init() {
54 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
55 "root of temp directory to put the sandbox into")
Colin Crosse52c2ac2022-03-28 17:03:35 -070056 flag.StringVar(&outputDir, "output-dir", "",
57 "directory which will contain all output files and only output files")
Colin Crosse16ce362020-11-12 08:29:30 -080058 flag.StringVar(&manifestFile, "manifest", "",
59 "textproto manifest describing the sandboxed command(s)")
Jeff Gaston93f0f372017-11-01 13:33:02 -070060 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
61 "whether to keep the sandbox directory when done")
Colin Crosse52c2ac2022-03-28 17:03:35 -070062 flag.BoolVar(&writeIfChanged, "write-if-changed", false,
63 "only write the output files if they have changed")
Jeff Gaston93f0f372017-11-01 13:33:02 -070064}
65
66func usageViolation(violation string) {
67 if violation != "" {
68 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
69 }
70
71 fmt.Fprintf(os.Stderr,
Colin Crosse16ce362020-11-12 08:29:30 -080072 "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070073
74 flag.PrintDefaults()
75
76 os.Exit(1)
77}
78
Jeff Gastonefc1b412017-03-29 17:29:06 -070079func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070080 flag.Usage = func() {
81 usageViolation("")
82 }
83 flag.Parse()
84
Jeff Gastonefc1b412017-03-29 17:29:06 -070085 error := run()
86 if error != nil {
87 fmt.Fprintln(os.Stderr, error)
88 os.Exit(1)
89 }
90}
91
Jeff Gaston90cfb092017-09-26 16:46:10 -070092func findAllFilesUnder(root string) (paths []string) {
93 paths = []string{}
94 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
95 if !info.IsDir() {
96 relPath, err := filepath.Rel(root, path)
97 if err != nil {
98 // couldn't find relative path from ancestor?
99 panic(err)
100 }
101 paths = append(paths, relPath)
102 }
103 return nil
104 })
105 return paths
106}
107
Jeff Gastonefc1b412017-03-29 17:29:06 -0700108func run() error {
Colin Crosse16ce362020-11-12 08:29:30 -0800109 if manifestFile == "" {
110 usageViolation("--manifest <manifest> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700111 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700112 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700113 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
114 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
115 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
116 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
117 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700118 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700119 }
Colin Crosse16ce362020-11-12 08:29:30 -0800120
121 manifest, err := readManifest(manifestFile)
122
123 if len(manifest.Commands) == 0 {
124 return fmt.Errorf("at least one commands entry is required in %q", manifestFile)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700125 }
126
Colin Crosse16ce362020-11-12 08:29:30 -0800127 // setup sandbox directory
128 err = os.MkdirAll(sandboxesRoot, 0777)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800129 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800130 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800131 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700132
Ulf Adamsb73daa52020-11-25 23:09:09 +0100133 // This tool assumes that there are no two concurrent runs with the same
134 // manifestFile. It should therefore be safe to use the hash of the
135 // manifestFile as the temporary directory name. We do this because it
136 // makes the temporary directory name deterministic. There are some
137 // tools that embed the name of the temporary output in the output, and
138 // they otherwise cause non-determinism, which then poisons actions
139 // depending on this one.
140 hash := sha1.New()
141 hash.Write([]byte(manifestFile))
142 tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil)))
143
144 err = os.RemoveAll(tempDir)
145 if err != nil {
146 return err
147 }
148 err = os.MkdirAll(tempDir, 0777)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700149 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800150 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700151 }
152
153 // In the common case, the following line of code is what removes the sandbox
154 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
Colin Crosse16ce362020-11-12 08:29:30 -0800155 // then at the beginning of the next build, Soong will wipe the temporary
156 // directory.
Jeff Gastonf49082a2017-06-07 13:22:22 -0700157 defer func() {
158 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700159 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700160 os.RemoveAll(tempDir)
161 }
162 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700163
Colin Crosse16ce362020-11-12 08:29:30 -0800164 // If there is more than one command in the manifest use a separate directory for each one.
165 useSubDir := len(manifest.Commands) > 1
166 var commandDepFiles []string
Jeff Gastonefc1b412017-03-29 17:29:06 -0700167
Colin Crosse16ce362020-11-12 08:29:30 -0800168 for i, command := range manifest.Commands {
169 localTempDir := tempDir
170 if useSubDir {
171 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700172 }
Kevin DuBois0a5da702021-10-05 15:41:02 -0700173 depFile, err := runCommand(command, localTempDir, i)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700174 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800175 // Running the command failed, keep the temporary output directory around in
176 // case a user wants to inspect it for debugging purposes. Soong will delete
177 // it at the beginning of the next build anyway.
178 keepOutDir = true
Jeff Gaston02a684b2017-10-27 14:59:27 -0700179 return err
180 }
Colin Crosse16ce362020-11-12 08:29:30 -0800181 if depFile != "" {
182 commandDepFiles = append(commandDepFiles, depFile)
183 }
184 }
185
186 outputDepFile := manifest.GetOutputDepfile()
187 if len(commandDepFiles) > 0 && outputDepFile == "" {
188 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file",
189 depFilePlaceholder)
190 }
191
192 if outputDepFile != "" {
193 // Merge the depfiles from each command in the manifest to a single output depfile.
194 err = rewriteDepFiles(commandDepFiles, outputDepFile)
195 if err != nil {
196 return fmt.Errorf("failed merging depfiles: %w", err)
197 }
198 }
199
200 return nil
201}
202
Kevin DuBois0a5da702021-10-05 15:41:02 -0700203// createCommandScript will create and return an exec.Cmd that runs rawCommand.
204//
205// rawCommand is executed via a script in the sandbox.
Colin Cross40cade42022-04-04 18:16:04 -0700206// scriptPath is the temporary where the script is created.
207// scriptPathInSandbox is the path to the script in the sbox environment.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700208//
209// returns an exec.Cmd that can be ran from within sbox context if no error, or nil if error.
210// caller must ensure script is cleaned up if function succeeds.
211//
Colin Cross40cade42022-04-04 18:16:04 -0700212func createCommandScript(rawCommand, scriptPath, scriptPathInSandbox string) (*exec.Cmd, error) {
213 err := os.WriteFile(scriptPath, []byte(rawCommand), 0644)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700214 if err != nil {
215 return nil, fmt.Errorf("failed to write command %s... to %s",
Colin Cross40cade42022-04-04 18:16:04 -0700216 rawCommand[0:40], scriptPath)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700217 }
Colin Cross40cade42022-04-04 18:16:04 -0700218 return exec.Command("bash", scriptPathInSandbox), nil
Kevin DuBois0a5da702021-10-05 15:41:02 -0700219}
220
Colin Crosse16ce362020-11-12 08:29:30 -0800221// readManifest reads an sbox manifest from a textproto file.
222func readManifest(file string) (*sbox_proto.Manifest, error) {
223 manifestData, err := ioutil.ReadFile(file)
224 if err != nil {
225 return nil, fmt.Errorf("error reading manifest %q: %w", file, err)
226 }
227
228 manifest := sbox_proto.Manifest{}
229
Dan Willemsen4591b642021-05-24 14:24:12 -0700230 err = prototext.Unmarshal(manifestData, &manifest)
Colin Crosse16ce362020-11-12 08:29:30 -0800231 if err != nil {
232 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err)
233 }
234
235 return &manifest, nil
236}
237
238// runCommand runs a single command from a manifest. If the command references the
239// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700240func runCommand(command *sbox_proto.Command, tempDir string, commandIndex int) (depFile string, err error) {
Colin Crosse16ce362020-11-12 08:29:30 -0800241 rawCommand := command.GetCommand()
242 if rawCommand == "" {
243 return "", fmt.Errorf("command is required")
244 }
245
Colin Crosse52c2ac2022-03-28 17:03:35 -0700246 // Remove files from the output directory
247 err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged))
248 if err != nil {
249 return "", err
250 }
251
Colin Crosse55bd422021-03-23 13:44:30 -0700252 pathToTempDirInSbox := tempDir
253 if command.GetChdir() {
254 pathToTempDirInSbox = "."
255 }
256
Colin Crosse16ce362020-11-12 08:29:30 -0800257 err = os.MkdirAll(tempDir, 0777)
258 if err != nil {
259 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
260 }
261
262 // Copy in any files specified by the manifest.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700263 err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite)
Colin Crosse16ce362020-11-12 08:29:30 -0800264 if err != nil {
265 return "", err
266 }
Colin Crosse55bd422021-03-23 13:44:30 -0700267 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox)
268 if err != nil {
269 return "", err
Colin Crossc590ec42021-03-11 17:20:02 -0800270 }
271
Colin Crosse16ce362020-11-12 08:29:30 -0800272 if strings.Contains(rawCommand, depFilePlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800273 depFile = filepath.Join(pathToTempDirInSbox, "deps.d")
Colin Crosse16ce362020-11-12 08:29:30 -0800274 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
275 }
276
277 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800278 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1)
Colin Crosse16ce362020-11-12 08:29:30 -0800279 }
280
281 // Emulate ninja's behavior of creating the directories for any output files before
282 // running the command.
283 err = makeOutputDirs(command.CopyAfter, tempDir)
284 if err != nil {
285 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700286 }
287
Colin Cross40cade42022-04-04 18:16:04 -0700288 scriptName := fmt.Sprintf("sbox_command.%d.bash", commandIndex)
289 scriptPath := joinPath(tempDir, scriptName)
290 scriptPathInSandbox := joinPath(pathToTempDirInSbox, scriptName)
291 cmd, err := createCommandScript(rawCommand, scriptPath, scriptPathInSandbox)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700292 if err != nil {
293 return "", err
294 }
295
Colin Cross4258a392021-04-15 18:50:11 -0700296 buf := &bytes.Buffer{}
Jeff Gastonefc1b412017-03-29 17:29:06 -0700297 cmd.Stdin = os.Stdin
Colin Cross4258a392021-04-15 18:50:11 -0700298 cmd.Stdout = buf
299 cmd.Stderr = buf
Colin Crosse16ce362020-11-12 08:29:30 -0800300
301 if command.GetChdir() {
302 cmd.Dir = tempDir
Colin Crossc590ec42021-03-11 17:20:02 -0800303 path := os.Getenv("PATH")
304 absPath, err := makeAbsPathEnv(path)
305 if err != nil {
306 return "", err
307 }
308 err = os.Setenv("PATH", absPath)
309 if err != nil {
310 return "", fmt.Errorf("Failed to update PATH: %w", err)
311 }
Colin Crosse16ce362020-11-12 08:29:30 -0800312 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700313 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700314
Colin Crossfc2d8422021-04-15 18:36:42 -0700315 if err != nil {
316 // The command failed, do a best effort copy of output files out of the sandbox. This is
317 // especially useful for linters with baselines that print an error message on failure
318 // with a command to copy the output lint errors to the new baseline. Use a copy instead of
319 // a move to leave the sandbox intact for manual inspection
Colin Crosse52c2ac2022-03-28 17:03:35 -0700320 copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged))
Colin Crossfc2d8422021-04-15 18:36:42 -0700321 }
322
Colin Cross4258a392021-04-15 18:50:11 -0700323 // If the command was executed but failed with an error, print a debugging message before
324 // the command's output so it doesn't scroll the real error message off the screen.
Jeff Gastonefc1b412017-03-29 17:29:06 -0700325 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Cross4258a392021-04-15 18:50:11 -0700326 fmt.Fprintf(os.Stderr,
327 "The failing command was run inside an sbox sandbox in temporary directory\n"+
328 "%s\n"+
Colin Cross40cade42022-04-04 18:16:04 -0700329 "The failing command line can be found in\n"+
Colin Cross4258a392021-04-15 18:50:11 -0700330 "%s\n",
Colin Cross40cade42022-04-04 18:16:04 -0700331 tempDir, scriptPath)
Colin Cross4258a392021-04-15 18:50:11 -0700332 }
333
334 // Write the command's combined stdout/stderr.
335 os.Stdout.Write(buf.Bytes())
336
337 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800338 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700339 }
340
Colin Crosse52c2ac2022-03-28 17:03:35 -0700341 err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand)
342 if err != nil {
343 return "", err
Jeff Gastonf49082a2017-06-07 13:22:22 -0700344 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700345
Jeff Gastonf49082a2017-06-07 13:22:22 -0700346 // the created files match the declared files; now move them
Colin Crosse52c2ac2022-03-28 17:03:35 -0700347 err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged))
348 if err != nil {
349 return "", err
350 }
Colin Crosse16ce362020-11-12 08:29:30 -0800351
352 return depFile, nil
353}
354
355// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
356// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
357// so that the tools don't have to.
358func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
359 for _, copyPair := range copies {
360 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
361 err := os.MkdirAll(dir, 0777)
362 if err != nil {
363 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700364 }
Colin Crosse16ce362020-11-12 08:29:30 -0800365 }
366 return nil
367}
368
369// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
370// were created by the command.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700371func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800372 var missingOutputErrors []error
Colin Crosse52c2ac2022-03-28 17:03:35 -0700373 var incorrectOutputDirectoryErrors []error
Colin Crosse16ce362020-11-12 08:29:30 -0800374 for _, copyPair := range copies {
375 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
376 fileInfo, err := os.Stat(fromPath)
377 if err != nil {
378 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
379 continue
380 }
381 if fileInfo.IsDir() {
382 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
383 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700384
385 toPath := copyPair.GetTo()
386 if rel, err := filepath.Rel(outputDir, toPath); err != nil {
387 return err
388 } else if strings.HasPrefix(rel, "../") {
389 incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors,
390 fmt.Errorf("%s is not under %s", toPath, outputDir))
391 }
Colin Crosse16ce362020-11-12 08:29:30 -0800392 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700393
394 const maxErrors = 10
395
396 if len(incorrectOutputDirectoryErrors) > 0 {
397 errorMessage := ""
398 more := 0
399 if len(incorrectOutputDirectoryErrors) > maxErrors {
400 more = len(incorrectOutputDirectoryErrors) - maxErrors
401 incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors]
402 }
403
404 for _, err := range incorrectOutputDirectoryErrors {
405 errorMessage += err.Error() + "\n"
406 }
407 if more > 0 {
408 errorMessage += fmt.Sprintf("...%v more", more)
409 }
410
411 return errors.New(errorMessage)
412 }
413
414 if len(missingOutputErrors) > 0 {
415 // find all created files for making a more informative error message
416 createdFiles := findAllFilesUnder(sandboxDir)
417
418 // build error message
419 errorMessage := "mismatch between declared and actual outputs\n"
420 errorMessage += "in sbox command(" + rawCommand + ")\n\n"
421 errorMessage += "in sandbox " + sandboxDir + ",\n"
422 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
423 for _, missingOutputError := range missingOutputErrors {
424 errorMessage += " " + missingOutputError.Error() + "\n"
425 }
426 if len(createdFiles) < 1 {
427 errorMessage += "created 0 files."
428 } else {
429 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
430 creationMessages := createdFiles
431 if len(creationMessages) > maxErrors {
432 creationMessages = creationMessages[:maxErrors]
433 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors))
434 }
435 for _, creationMessage := range creationMessages {
436 errorMessage += " " + creationMessage + "\n"
437 }
438 }
439
440 return errors.New(errorMessage)
441 }
442
443 return nil
Colin Crosse16ce362020-11-12 08:29:30 -0800444}
445
Colin Crosse52c2ac2022-03-28 17:03:35 -0700446type existsType bool
447
448const (
449 requireFromExists existsType = false
450 allowFromNotExists = true
451)
452
453type writeType bool
454
455const (
456 alwaysWrite writeType = false
457 onlyWriteIfChanged = true
458)
459
460// copyFiles copies files in or out of the sandbox. If exists is allowFromNotExists then errors
461// caused by a from path not existing are ignored. If write is onlyWriteIfChanged then the output
462// file is compared to the input file and not written to if it is the same, avoiding updating
463// the timestamp.
464func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800465 for _, copyPair := range copies {
466 fromPath := joinPath(fromDir, copyPair.GetFrom())
467 toPath := joinPath(toDir, copyPair.GetTo())
Colin Crosse52c2ac2022-03-28 17:03:35 -0700468 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write)
Colin Crosse16ce362020-11-12 08:29:30 -0800469 if err != nil {
470 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
471 }
472 }
473 return nil
474}
475
Colin Crossfc2d8422021-04-15 18:36:42 -0700476// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the
Colin Crosse52c2ac2022-03-28 17:03:35 -0700477// permissions. If exists is allowFromNotExists it returns nil if the from path doesn't exist.
478// If write is onlyWriteIfChanged then the output file is compared to the input file and not written to
479// if it is the same, avoiding updating the timestamp.
480func copyOneFile(from string, to string, forceExecutable bool, exists existsType,
481 write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800482 err := os.MkdirAll(filepath.Dir(to), 0777)
483 if err != nil {
484 return err
485 }
486
Colin Crosse16ce362020-11-12 08:29:30 -0800487 stat, err := os.Stat(from)
488 if err != nil {
Colin Crosse52c2ac2022-03-28 17:03:35 -0700489 if os.IsNotExist(err) && exists == allowFromNotExists {
Colin Crossfc2d8422021-04-15 18:36:42 -0700490 return nil
491 }
Colin Crosse16ce362020-11-12 08:29:30 -0800492 return err
493 }
494
495 perm := stat.Mode()
Colin Crossfc2d8422021-04-15 18:36:42 -0700496 if forceExecutable {
Colin Cross859dfd92020-11-30 20:12:47 -0800497 perm = perm | 0100 // u+x
498 }
Colin Crosse16ce362020-11-12 08:29:30 -0800499
Colin Crosse52c2ac2022-03-28 17:03:35 -0700500 if write == onlyWriteIfChanged && filesHaveSameContents(from, to) {
501 return nil
502 }
503
Colin Crosse16ce362020-11-12 08:29:30 -0800504 in, err := os.Open(from)
505 if err != nil {
506 return err
507 }
508 defer in.Close()
509
Colin Cross607c0b72021-03-31 12:54:06 -0700510 // Remove the target before copying. In most cases the file won't exist, but if there are
511 // duplicate copy rules for a file and the source file was read-only the second copy could
512 // fail.
513 err = os.Remove(to)
514 if err != nil && !os.IsNotExist(err) {
515 return err
516 }
517
Colin Crosse16ce362020-11-12 08:29:30 -0800518 out, err := os.Create(to)
519 if err != nil {
520 return err
521 }
522 defer func() {
523 out.Close()
524 if err != nil {
525 os.Remove(to)
526 }
527 }()
528
529 _, err = io.Copy(out, in)
530 if err != nil {
531 return err
532 }
533
534 if err = out.Close(); err != nil {
535 return err
536 }
537
538 if err = os.Chmod(to, perm); err != nil {
539 return err
540 }
541
542 return nil
543}
544
Colin Crosse55bd422021-03-23 13:44:30 -0700545// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files
546// listed into the sandbox.
547func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error {
548 for _, rspFile := range rspFiles {
549 err := copyOneRspFile(rspFile, toDir, toDirInSandbox)
550 if err != nil {
551 return err
552 }
553 }
554 return nil
555}
556
557// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files
558// listed into the sandbox.
559func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error {
560 in, err := os.Open(rspFile.GetFile())
561 if err != nil {
562 return err
563 }
564 defer in.Close()
565
566 files, err := response.ReadRspFile(in)
567 if err != nil {
568 return err
569 }
570
571 for i, from := range files {
572 // Convert the real path of the input file into the path inside the sandbox using the
573 // path mappings.
574 to := applyPathMappings(rspFile.PathMappings, from)
575
576 // Copy the file into the sandbox.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700577 err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite)
Colin Crosse55bd422021-03-23 13:44:30 -0700578 if err != nil {
579 return err
580 }
581
582 // Rewrite the name in the list of files to be relative to the sandbox directory.
583 files[i] = joinPath(toDirInSandbox, to)
584 }
585
586 // Convert the real path of the rsp file into the path inside the sandbox using the path
587 // mappings.
588 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile()))
589
590 err = os.MkdirAll(filepath.Dir(outRspFile), 0777)
591 if err != nil {
592 return err
593 }
594
595 out, err := os.Create(outRspFile)
596 if err != nil {
597 return err
598 }
599 defer out.Close()
600
601 // Write the rsp file with converted paths into the sandbox.
602 err = response.WriteRspFile(out, files)
603 if err != nil {
604 return err
605 }
606
607 return nil
608}
609
610// applyPathMappings takes a list of path mappings and a path, and returns the path with the first
611// matching path mapping applied. If the path does not match any of the path mappings then it is
612// returned unmodified.
613func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string {
614 for _, mapping := range pathMappings {
615 if strings.HasPrefix(path, mapping.GetFrom()+"/") {
616 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/"))
617 }
618 }
619 return path
620}
621
Colin Crosse16ce362020-11-12 08:29:30 -0800622// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
623// to moving files where the source and destination are in the same filesystem. This is OK for
Colin Crosse52c2ac2022-03-28 17:03:35 -0700624// sbox because the temporary directory is inside the out directory. If write is onlyWriteIfChanged
625// then the output file is compared to the input file and not written to if it is the same, avoiding
626// updating the timestamp. Otherwise it always updates the timestamp of the new file.
627func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800628 for _, copyPair := range copies {
629 fromPath := joinPath(fromDir, copyPair.GetFrom())
630 toPath := joinPath(toDir, copyPair.GetTo())
631 err := os.MkdirAll(filepath.Dir(toPath), 0777)
632 if err != nil {
633 return err
634 }
635
Colin Crosse52c2ac2022-03-28 17:03:35 -0700636 if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) {
637 continue
638 }
639
Colin Crosse16ce362020-11-12 08:29:30 -0800640 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800641 if err != nil {
642 return err
643 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700644
645 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
646 // files with old timestamps).
647 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800648 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700649 if err != nil {
650 return err
651 }
652 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700653 return nil
654}
Colin Crosse16ce362020-11-12 08:29:30 -0800655
Colin Crosse52c2ac2022-03-28 17:03:35 -0700656// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or
657// any files not listed in copies if write is onlyWriteIfChanged
658func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error {
659 if outputDir == "" {
660 return fmt.Errorf("output directory must be set")
661 }
662
663 if write == alwaysWrite {
664 // When writing all the output files remove the whole output directory
665 return os.RemoveAll(outputDir)
666 }
667
668 outputFiles := make(map[string]bool, len(copies))
669 for _, copyPair := range copies {
670 outputFiles[copyPair.GetTo()] = true
671 }
672
673 existingFiles := findAllFilesUnder(outputDir)
674 for _, existingFile := range existingFiles {
675 fullExistingFile := filepath.Join(outputDir, existingFile)
676 if !outputFiles[fullExistingFile] {
677 err := os.Remove(fullExistingFile)
678 if err != nil {
679 return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err)
680 }
681 }
682 }
683
684 return nil
685}
686
Colin Crosse16ce362020-11-12 08:29:30 -0800687// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
688// to an output file.
689func rewriteDepFiles(ins []string, out string) error {
690 var mergedDeps []string
691 for _, in := range ins {
692 data, err := ioutil.ReadFile(in)
693 if err != nil {
694 return err
695 }
696
697 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
698 if err != nil {
699 return err
700 }
701 mergedDeps = append(mergedDeps, deps.Inputs...)
702 }
703
704 deps := makedeps.Deps{
705 // Ninja doesn't care what the output file is, so we can use any string here.
706 Output: "outputfile",
707 Inputs: mergedDeps,
708 }
709
710 // Make the directory for the output depfile in case it is in a different directory
711 // than any of the output files.
712 outDir := filepath.Dir(out)
713 err := os.MkdirAll(outDir, 0777)
714 if err != nil {
715 return fmt.Errorf("failed to create %q: %w", outDir, err)
716 }
717
718 return ioutil.WriteFile(out, deps.Print(), 0666)
719}
720
721// joinPath wraps filepath.Join but returns file without appending to dir if file is
722// absolute.
723func joinPath(dir, file string) string {
724 if filepath.IsAbs(file) {
725 return file
726 }
727 return filepath.Join(dir, file)
728}
Colin Crossc590ec42021-03-11 17:20:02 -0800729
Colin Crosse52c2ac2022-03-28 17:03:35 -0700730// filesHaveSameContents compares the contents if two files, returning true if they are the same
731// and returning false if they are different or any errors occur.
732func filesHaveSameContents(a, b string) bool {
733 // Compare the sizes of the two files
734 statA, err := os.Stat(a)
735 if err != nil {
736 return false
737 }
738 statB, err := os.Stat(b)
739 if err != nil {
740 return false
741 }
742
743 if statA.Size() != statB.Size() {
744 return false
745 }
746
747 // Open the two files
748 fileA, err := os.Open(a)
749 if err != nil {
750 return false
751 }
752 defer fileA.Close()
753 fileB, err := os.Open(a)
754 if err != nil {
755 return false
756 }
757 defer fileB.Close()
758
759 // Compare the files 1MB at a time
760 const bufSize = 1 * 1024 * 1024
761 bufA := make([]byte, bufSize)
762 bufB := make([]byte, bufSize)
763
764 remain := statA.Size()
765 for remain > 0 {
766 toRead := int64(bufSize)
767 if toRead > remain {
768 toRead = remain
769 }
770
771 _, err = io.ReadFull(fileA, bufA[:toRead])
772 if err != nil {
773 return false
774 }
775 _, err = io.ReadFull(fileB, bufB[:toRead])
776 if err != nil {
777 return false
778 }
779
780 if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 {
781 return false
782 }
783
784 remain -= toRead
785 }
786
787 return true
788}
789
Colin Crossc590ec42021-03-11 17:20:02 -0800790func makeAbsPathEnv(pathEnv string) (string, error) {
791 pathEnvElements := filepath.SplitList(pathEnv)
792 for i, p := range pathEnvElements {
793 if !filepath.IsAbs(p) {
794 absPath, err := filepath.Abs(p)
795 if err != nil {
796 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err)
797 }
798 pathEnvElements[i] = absPath
799 }
800 }
801 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil
802}