blob: 3364f503f9f4de0e8a12e224729e885b9483766a [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)
Sam Delmerico0e2d63e2023-09-11 17:18:08 +0000122 if err != nil {
123 return err
124 }
Colin Crosse16ce362020-11-12 08:29:30 -0800125
126 if len(manifest.Commands) == 0 {
127 return fmt.Errorf("at least one commands entry is required in %q", manifestFile)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700128 }
129
Colin Crosse16ce362020-11-12 08:29:30 -0800130 // setup sandbox directory
131 err = os.MkdirAll(sandboxesRoot, 0777)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800132 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800133 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800134 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700135
Ulf Adamsb73daa52020-11-25 23:09:09 +0100136 // This tool assumes that there are no two concurrent runs with the same
137 // manifestFile. It should therefore be safe to use the hash of the
138 // manifestFile as the temporary directory name. We do this because it
139 // makes the temporary directory name deterministic. There are some
140 // tools that embed the name of the temporary output in the output, and
141 // they otherwise cause non-determinism, which then poisons actions
142 // depending on this one.
143 hash := sha1.New()
144 hash.Write([]byte(manifestFile))
145 tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil)))
146
147 err = os.RemoveAll(tempDir)
148 if err != nil {
149 return err
150 }
151 err = os.MkdirAll(tempDir, 0777)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700152 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800153 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700154 }
155
156 // In the common case, the following line of code is what removes the sandbox
157 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
Colin Crosse16ce362020-11-12 08:29:30 -0800158 // then at the beginning of the next build, Soong will wipe the temporary
159 // directory.
Jeff Gastonf49082a2017-06-07 13:22:22 -0700160 defer func() {
161 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700162 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700163 os.RemoveAll(tempDir)
164 }
165 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700166
Colin Crosse16ce362020-11-12 08:29:30 -0800167 // If there is more than one command in the manifest use a separate directory for each one.
168 useSubDir := len(manifest.Commands) > 1
169 var commandDepFiles []string
Jeff Gastonefc1b412017-03-29 17:29:06 -0700170
Colin Crosse16ce362020-11-12 08:29:30 -0800171 for i, command := range manifest.Commands {
172 localTempDir := tempDir
173 if useSubDir {
174 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700175 }
Kevin DuBois0a5da702021-10-05 15:41:02 -0700176 depFile, err := runCommand(command, localTempDir, i)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700177 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800178 // Running the command failed, keep the temporary output directory around in
179 // case a user wants to inspect it for debugging purposes. Soong will delete
180 // it at the beginning of the next build anyway.
181 keepOutDir = true
Jeff Gaston02a684b2017-10-27 14:59:27 -0700182 return err
183 }
Colin Crosse16ce362020-11-12 08:29:30 -0800184 if depFile != "" {
185 commandDepFiles = append(commandDepFiles, depFile)
186 }
187 }
188
189 outputDepFile := manifest.GetOutputDepfile()
190 if len(commandDepFiles) > 0 && outputDepFile == "" {
191 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file",
192 depFilePlaceholder)
193 }
194
195 if outputDepFile != "" {
196 // Merge the depfiles from each command in the manifest to a single output depfile.
197 err = rewriteDepFiles(commandDepFiles, outputDepFile)
198 if err != nil {
199 return fmt.Errorf("failed merging depfiles: %w", err)
200 }
201 }
202
203 return nil
204}
205
Kevin DuBois0a5da702021-10-05 15:41:02 -0700206// createCommandScript will create and return an exec.Cmd that runs rawCommand.
207//
208// rawCommand is executed via a script in the sandbox.
Colin Cross40cade42022-04-04 18:16:04 -0700209// scriptPath is the temporary where the script is created.
210// scriptPathInSandbox is the path to the script in the sbox environment.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700211//
212// returns an exec.Cmd that can be ran from within sbox context if no error, or nil if error.
213// caller must ensure script is cleaned up if function succeeds.
Colin Cross40cade42022-04-04 18:16:04 -0700214func createCommandScript(rawCommand, scriptPath, scriptPathInSandbox string) (*exec.Cmd, error) {
215 err := os.WriteFile(scriptPath, []byte(rawCommand), 0644)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700216 if err != nil {
217 return nil, fmt.Errorf("failed to write command %s... to %s",
Colin Cross40cade42022-04-04 18:16:04 -0700218 rawCommand[0:40], scriptPath)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700219 }
Colin Cross40cade42022-04-04 18:16:04 -0700220 return exec.Command("bash", scriptPathInSandbox), nil
Kevin DuBois0a5da702021-10-05 15:41:02 -0700221}
222
Colin Crosse16ce362020-11-12 08:29:30 -0800223// readManifest reads an sbox manifest from a textproto file.
224func readManifest(file string) (*sbox_proto.Manifest, error) {
225 manifestData, err := ioutil.ReadFile(file)
226 if err != nil {
227 return nil, fmt.Errorf("error reading manifest %q: %w", file, err)
228 }
229
230 manifest := sbox_proto.Manifest{}
231
Dan Willemsen4591b642021-05-24 14:24:12 -0700232 err = prototext.Unmarshal(manifestData, &manifest)
Colin Crosse16ce362020-11-12 08:29:30 -0800233 if err != nil {
234 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err)
235 }
236
237 return &manifest, nil
238}
239
240// runCommand runs a single command from a manifest. If the command references the
241// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700242func runCommand(command *sbox_proto.Command, tempDir string, commandIndex int) (depFile string, err error) {
Colin Crosse16ce362020-11-12 08:29:30 -0800243 rawCommand := command.GetCommand()
244 if rawCommand == "" {
245 return "", fmt.Errorf("command is required")
246 }
247
Colin Crosse52c2ac2022-03-28 17:03:35 -0700248 // Remove files from the output directory
249 err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged))
250 if err != nil {
251 return "", err
252 }
253
Colin Crosse55bd422021-03-23 13:44:30 -0700254 pathToTempDirInSbox := tempDir
255 if command.GetChdir() {
256 pathToTempDirInSbox = "."
257 }
258
Colin Crosse16ce362020-11-12 08:29:30 -0800259 err = os.MkdirAll(tempDir, 0777)
260 if err != nil {
261 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
262 }
263
264 // Copy in any files specified by the manifest.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700265 err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite)
Colin Crosse16ce362020-11-12 08:29:30 -0800266 if err != nil {
267 return "", err
268 }
Colin Crosse55bd422021-03-23 13:44:30 -0700269 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox)
270 if err != nil {
271 return "", err
Colin Crossc590ec42021-03-11 17:20:02 -0800272 }
273
Colin Crosse16ce362020-11-12 08:29:30 -0800274 if strings.Contains(rawCommand, depFilePlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800275 depFile = filepath.Join(pathToTempDirInSbox, "deps.d")
Colin Crosse16ce362020-11-12 08:29:30 -0800276 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
277 }
278
279 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800280 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1)
Colin Crosse16ce362020-11-12 08:29:30 -0800281 }
282
283 // Emulate ninja's behavior of creating the directories for any output files before
284 // running the command.
285 err = makeOutputDirs(command.CopyAfter, tempDir)
286 if err != nil {
287 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700288 }
289
Colin Cross40cade42022-04-04 18:16:04 -0700290 scriptName := fmt.Sprintf("sbox_command.%d.bash", commandIndex)
291 scriptPath := joinPath(tempDir, scriptName)
292 scriptPathInSandbox := joinPath(pathToTempDirInSbox, scriptName)
293 cmd, err := createCommandScript(rawCommand, scriptPath, scriptPathInSandbox)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700294 if err != nil {
295 return "", err
296 }
297
Colin Cross4258a392021-04-15 18:50:11 -0700298 buf := &bytes.Buffer{}
Jeff Gastonefc1b412017-03-29 17:29:06 -0700299 cmd.Stdin = os.Stdin
Colin Cross4258a392021-04-15 18:50:11 -0700300 cmd.Stdout = buf
301 cmd.Stderr = buf
Colin Crosse16ce362020-11-12 08:29:30 -0800302
303 if command.GetChdir() {
304 cmd.Dir = tempDir
Colin Crossc590ec42021-03-11 17:20:02 -0800305 path := os.Getenv("PATH")
306 absPath, err := makeAbsPathEnv(path)
307 if err != nil {
308 return "", err
309 }
310 err = os.Setenv("PATH", absPath)
311 if err != nil {
312 return "", fmt.Errorf("Failed to update PATH: %w", err)
313 }
Colin Crosse16ce362020-11-12 08:29:30 -0800314 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700315 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700316
Colin Crossfc2d8422021-04-15 18:36:42 -0700317 if err != nil {
318 // The command failed, do a best effort copy of output files out of the sandbox. This is
319 // especially useful for linters with baselines that print an error message on failure
320 // with a command to copy the output lint errors to the new baseline. Use a copy instead of
321 // a move to leave the sandbox intact for manual inspection
Colin Crosse52c2ac2022-03-28 17:03:35 -0700322 copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged))
Colin Crossfc2d8422021-04-15 18:36:42 -0700323 }
324
Colin Cross4258a392021-04-15 18:50:11 -0700325 // If the command was executed but failed with an error, print a debugging message before
326 // the command's output so it doesn't scroll the real error message off the screen.
Jeff Gastonefc1b412017-03-29 17:29:06 -0700327 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Cross4258a392021-04-15 18:50:11 -0700328 fmt.Fprintf(os.Stderr,
329 "The failing command was run inside an sbox sandbox in temporary directory\n"+
330 "%s\n"+
Colin Cross40cade42022-04-04 18:16:04 -0700331 "The failing command line can be found in\n"+
Colin Cross4258a392021-04-15 18:50:11 -0700332 "%s\n",
Colin Cross40cade42022-04-04 18:16:04 -0700333 tempDir, scriptPath)
Colin Cross4258a392021-04-15 18:50:11 -0700334 }
335
336 // Write the command's combined stdout/stderr.
337 os.Stdout.Write(buf.Bytes())
338
339 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800340 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700341 }
342
Colin Crosse52c2ac2022-03-28 17:03:35 -0700343 err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand)
344 if err != nil {
345 return "", err
Jeff Gastonf49082a2017-06-07 13:22:22 -0700346 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700347
Jeff Gastonf49082a2017-06-07 13:22:22 -0700348 // the created files match the declared files; now move them
Colin Crosse52c2ac2022-03-28 17:03:35 -0700349 err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged))
350 if err != nil {
351 return "", err
352 }
Colin Crosse16ce362020-11-12 08:29:30 -0800353
354 return depFile, nil
355}
356
357// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
358// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
359// so that the tools don't have to.
360func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
361 for _, copyPair := range copies {
362 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
363 err := os.MkdirAll(dir, 0777)
364 if err != nil {
365 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700366 }
Colin Crosse16ce362020-11-12 08:29:30 -0800367 }
368 return nil
369}
370
371// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
372// were created by the command.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700373func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800374 var missingOutputErrors []error
Colin Crosse52c2ac2022-03-28 17:03:35 -0700375 var incorrectOutputDirectoryErrors []error
Colin Crosse16ce362020-11-12 08:29:30 -0800376 for _, copyPair := range copies {
377 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
378 fileInfo, err := os.Stat(fromPath)
379 if err != nil {
380 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
381 continue
382 }
383 if fileInfo.IsDir() {
384 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
385 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700386
387 toPath := copyPair.GetTo()
388 if rel, err := filepath.Rel(outputDir, toPath); err != nil {
389 return err
390 } else if strings.HasPrefix(rel, "../") {
391 incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors,
392 fmt.Errorf("%s is not under %s", toPath, outputDir))
393 }
Colin Crosse16ce362020-11-12 08:29:30 -0800394 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700395
Steven Morelandee975ee2023-03-28 22:39:00 +0000396 const maxErrors = 25
Colin Crosse52c2ac2022-03-28 17:03:35 -0700397
398 if len(incorrectOutputDirectoryErrors) > 0 {
399 errorMessage := ""
400 more := 0
401 if len(incorrectOutputDirectoryErrors) > maxErrors {
402 more = len(incorrectOutputDirectoryErrors) - maxErrors
403 incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors]
404 }
405
406 for _, err := range incorrectOutputDirectoryErrors {
407 errorMessage += err.Error() + "\n"
408 }
409 if more > 0 {
410 errorMessage += fmt.Sprintf("...%v more", more)
411 }
412
413 return errors.New(errorMessage)
414 }
415
416 if len(missingOutputErrors) > 0 {
417 // find all created files for making a more informative error message
418 createdFiles := findAllFilesUnder(sandboxDir)
419
420 // build error message
421 errorMessage := "mismatch between declared and actual outputs\n"
422 errorMessage += "in sbox command(" + rawCommand + ")\n\n"
423 errorMessage += "in sandbox " + sandboxDir + ",\n"
424 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
425 for _, missingOutputError := range missingOutputErrors {
426 errorMessage += " " + missingOutputError.Error() + "\n"
427 }
428 if len(createdFiles) < 1 {
429 errorMessage += "created 0 files."
430 } else {
431 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
432 creationMessages := createdFiles
433 if len(creationMessages) > maxErrors {
434 creationMessages = creationMessages[:maxErrors]
435 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors))
436 }
437 for _, creationMessage := range creationMessages {
438 errorMessage += " " + creationMessage + "\n"
439 }
440 }
441
442 return errors.New(errorMessage)
443 }
444
445 return nil
Colin Crosse16ce362020-11-12 08:29:30 -0800446}
447
Colin Crosse52c2ac2022-03-28 17:03:35 -0700448type existsType bool
449
450const (
451 requireFromExists existsType = false
452 allowFromNotExists = true
453)
454
455type writeType bool
456
457const (
458 alwaysWrite writeType = false
459 onlyWriteIfChanged = true
460)
461
462// copyFiles copies files in or out of the sandbox. If exists is allowFromNotExists then errors
463// caused by a from path not existing are ignored. If write is onlyWriteIfChanged then the output
464// file is compared to the input file and not written to if it is the same, avoiding updating
465// the timestamp.
466func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800467 for _, copyPair := range copies {
468 fromPath := joinPath(fromDir, copyPair.GetFrom())
469 toPath := joinPath(toDir, copyPair.GetTo())
Colin Crosse52c2ac2022-03-28 17:03:35 -0700470 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write)
Colin Crosse16ce362020-11-12 08:29:30 -0800471 if err != nil {
472 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
473 }
474 }
475 return nil
476}
477
Colin Crossfc2d8422021-04-15 18:36:42 -0700478// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the
Colin Crosse52c2ac2022-03-28 17:03:35 -0700479// permissions. If exists is allowFromNotExists it returns nil if the from path doesn't exist.
480// If write is onlyWriteIfChanged then the output file is compared to the input file and not written to
481// if it is the same, avoiding updating the timestamp.
482func copyOneFile(from string, to string, forceExecutable bool, exists existsType,
483 write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800484 err := os.MkdirAll(filepath.Dir(to), 0777)
485 if err != nil {
486 return err
487 }
488
Colin Crosse16ce362020-11-12 08:29:30 -0800489 stat, err := os.Stat(from)
490 if err != nil {
Colin Crosse52c2ac2022-03-28 17:03:35 -0700491 if os.IsNotExist(err) && exists == allowFromNotExists {
Colin Crossfc2d8422021-04-15 18:36:42 -0700492 return nil
493 }
Colin Crosse16ce362020-11-12 08:29:30 -0800494 return err
495 }
496
497 perm := stat.Mode()
Colin Crossfc2d8422021-04-15 18:36:42 -0700498 if forceExecutable {
Colin Cross859dfd92020-11-30 20:12:47 -0800499 perm = perm | 0100 // u+x
500 }
Colin Crosse16ce362020-11-12 08:29:30 -0800501
Colin Crosse52c2ac2022-03-28 17:03:35 -0700502 if write == onlyWriteIfChanged && filesHaveSameContents(from, to) {
503 return nil
504 }
505
Colin Crosse16ce362020-11-12 08:29:30 -0800506 in, err := os.Open(from)
507 if err != nil {
508 return err
509 }
510 defer in.Close()
511
Colin Cross607c0b72021-03-31 12:54:06 -0700512 // Remove the target before copying. In most cases the file won't exist, but if there are
513 // duplicate copy rules for a file and the source file was read-only the second copy could
514 // fail.
515 err = os.Remove(to)
516 if err != nil && !os.IsNotExist(err) {
517 return err
518 }
519
Colin Crosse16ce362020-11-12 08:29:30 -0800520 out, err := os.Create(to)
521 if err != nil {
522 return err
523 }
524 defer func() {
525 out.Close()
526 if err != nil {
527 os.Remove(to)
528 }
529 }()
530
531 _, err = io.Copy(out, in)
532 if err != nil {
533 return err
534 }
535
536 if err = out.Close(); err != nil {
537 return err
538 }
539
540 if err = os.Chmod(to, perm); err != nil {
541 return err
542 }
543
544 return nil
545}
546
Colin Crosse55bd422021-03-23 13:44:30 -0700547// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files
548// listed into the sandbox.
549func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error {
550 for _, rspFile := range rspFiles {
551 err := copyOneRspFile(rspFile, toDir, toDirInSandbox)
552 if err != nil {
553 return err
554 }
555 }
556 return nil
557}
558
559// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files
560// listed into the sandbox.
561func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error {
562 in, err := os.Open(rspFile.GetFile())
563 if err != nil {
564 return err
565 }
566 defer in.Close()
567
568 files, err := response.ReadRspFile(in)
569 if err != nil {
570 return err
571 }
572
573 for i, from := range files {
574 // Convert the real path of the input file into the path inside the sandbox using the
575 // path mappings.
576 to := applyPathMappings(rspFile.PathMappings, from)
577
578 // Copy the file into the sandbox.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700579 err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite)
Colin Crosse55bd422021-03-23 13:44:30 -0700580 if err != nil {
581 return err
582 }
583
584 // Rewrite the name in the list of files to be relative to the sandbox directory.
585 files[i] = joinPath(toDirInSandbox, to)
586 }
587
588 // Convert the real path of the rsp file into the path inside the sandbox using the path
589 // mappings.
590 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile()))
591
592 err = os.MkdirAll(filepath.Dir(outRspFile), 0777)
593 if err != nil {
594 return err
595 }
596
597 out, err := os.Create(outRspFile)
598 if err != nil {
599 return err
600 }
601 defer out.Close()
602
603 // Write the rsp file with converted paths into the sandbox.
604 err = response.WriteRspFile(out, files)
605 if err != nil {
606 return err
607 }
608
609 return nil
610}
611
612// applyPathMappings takes a list of path mappings and a path, and returns the path with the first
613// matching path mapping applied. If the path does not match any of the path mappings then it is
614// returned unmodified.
615func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string {
616 for _, mapping := range pathMappings {
617 if strings.HasPrefix(path, mapping.GetFrom()+"/") {
618 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/"))
619 }
620 }
621 return path
622}
623
Colin Crosse16ce362020-11-12 08:29:30 -0800624// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
625// to moving files where the source and destination are in the same filesystem. This is OK for
Colin Crosse52c2ac2022-03-28 17:03:35 -0700626// sbox because the temporary directory is inside the out directory. If write is onlyWriteIfChanged
627// then the output file is compared to the input file and not written to if it is the same, avoiding
628// updating the timestamp. Otherwise it always updates the timestamp of the new file.
629func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800630 for _, copyPair := range copies {
631 fromPath := joinPath(fromDir, copyPair.GetFrom())
632 toPath := joinPath(toDir, copyPair.GetTo())
633 err := os.MkdirAll(filepath.Dir(toPath), 0777)
634 if err != nil {
635 return err
636 }
637
Colin Crosse52c2ac2022-03-28 17:03:35 -0700638 if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) {
639 continue
640 }
641
Colin Crosse16ce362020-11-12 08:29:30 -0800642 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800643 if err != nil {
644 return err
645 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700646
647 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
648 // files with old timestamps).
649 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800650 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700651 if err != nil {
652 return err
653 }
654 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700655 return nil
656}
Colin Crosse16ce362020-11-12 08:29:30 -0800657
Colin Crosse52c2ac2022-03-28 17:03:35 -0700658// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or
659// any files not listed in copies if write is onlyWriteIfChanged
660func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error {
661 if outputDir == "" {
662 return fmt.Errorf("output directory must be set")
663 }
664
665 if write == alwaysWrite {
666 // When writing all the output files remove the whole output directory
667 return os.RemoveAll(outputDir)
668 }
669
670 outputFiles := make(map[string]bool, len(copies))
671 for _, copyPair := range copies {
672 outputFiles[copyPair.GetTo()] = true
673 }
674
675 existingFiles := findAllFilesUnder(outputDir)
676 for _, existingFile := range existingFiles {
677 fullExistingFile := filepath.Join(outputDir, existingFile)
678 if !outputFiles[fullExistingFile] {
679 err := os.Remove(fullExistingFile)
680 if err != nil {
681 return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err)
682 }
683 }
684 }
685
686 return nil
687}
688
Colin Crosse16ce362020-11-12 08:29:30 -0800689// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
690// to an output file.
691func rewriteDepFiles(ins []string, out string) error {
692 var mergedDeps []string
693 for _, in := range ins {
694 data, err := ioutil.ReadFile(in)
695 if err != nil {
696 return err
697 }
698
699 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
700 if err != nil {
701 return err
702 }
703 mergedDeps = append(mergedDeps, deps.Inputs...)
704 }
705
706 deps := makedeps.Deps{
707 // Ninja doesn't care what the output file is, so we can use any string here.
708 Output: "outputfile",
709 Inputs: mergedDeps,
710 }
711
712 // Make the directory for the output depfile in case it is in a different directory
713 // than any of the output files.
714 outDir := filepath.Dir(out)
715 err := os.MkdirAll(outDir, 0777)
716 if err != nil {
717 return fmt.Errorf("failed to create %q: %w", outDir, err)
718 }
719
720 return ioutil.WriteFile(out, deps.Print(), 0666)
721}
722
723// joinPath wraps filepath.Join but returns file without appending to dir if file is
724// absolute.
725func joinPath(dir, file string) string {
726 if filepath.IsAbs(file) {
727 return file
728 }
729 return filepath.Join(dir, file)
730}
Colin Crossc590ec42021-03-11 17:20:02 -0800731
Colin Crosse52c2ac2022-03-28 17:03:35 -0700732// filesHaveSameContents compares the contents if two files, returning true if they are the same
733// and returning false if they are different or any errors occur.
734func filesHaveSameContents(a, b string) bool {
735 // Compare the sizes of the two files
736 statA, err := os.Stat(a)
737 if err != nil {
738 return false
739 }
740 statB, err := os.Stat(b)
741 if err != nil {
742 return false
743 }
744
745 if statA.Size() != statB.Size() {
746 return false
747 }
748
749 // Open the two files
750 fileA, err := os.Open(a)
751 if err != nil {
752 return false
753 }
754 defer fileA.Close()
Colin Crossfa8e9cc2022-04-12 17:26:58 -0700755 fileB, err := os.Open(b)
Colin Crosse52c2ac2022-03-28 17:03:35 -0700756 if err != nil {
757 return false
758 }
759 defer fileB.Close()
760
761 // Compare the files 1MB at a time
762 const bufSize = 1 * 1024 * 1024
763 bufA := make([]byte, bufSize)
764 bufB := make([]byte, bufSize)
765
766 remain := statA.Size()
767 for remain > 0 {
768 toRead := int64(bufSize)
769 if toRead > remain {
770 toRead = remain
771 }
772
773 _, err = io.ReadFull(fileA, bufA[:toRead])
774 if err != nil {
775 return false
776 }
777 _, err = io.ReadFull(fileB, bufB[:toRead])
778 if err != nil {
779 return false
780 }
781
782 if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 {
783 return false
784 }
785
786 remain -= toRead
787 }
788
789 return true
790}
791
Colin Crossc590ec42021-03-11 17:20:02 -0800792func makeAbsPathEnv(pathEnv string) (string, error) {
793 pathEnvElements := filepath.SplitList(pathEnv)
794 for i, p := range pathEnvElements {
795 if !filepath.IsAbs(p) {
796 absPath, err := filepath.Abs(p)
797 if err != nil {
798 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err)
799 }
800 pathEnvElements[i] = absPath
801 }
802 }
803 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil
804}