blob: fc56dd52631ff6a9733300e96b7fe3130d86b14c [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.
Colin Cross40cade42022-04-04 18:16:04 -0700211func createCommandScript(rawCommand, scriptPath, scriptPathInSandbox string) (*exec.Cmd, error) {
212 err := os.WriteFile(scriptPath, []byte(rawCommand), 0644)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700213 if err != nil {
214 return nil, fmt.Errorf("failed to write command %s... to %s",
Colin Cross40cade42022-04-04 18:16:04 -0700215 rawCommand[0:40], scriptPath)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700216 }
Colin Cross40cade42022-04-04 18:16:04 -0700217 return exec.Command("bash", scriptPathInSandbox), nil
Kevin DuBois0a5da702021-10-05 15:41:02 -0700218}
219
Colin Crosse16ce362020-11-12 08:29:30 -0800220// readManifest reads an sbox manifest from a textproto file.
221func readManifest(file string) (*sbox_proto.Manifest, error) {
222 manifestData, err := ioutil.ReadFile(file)
223 if err != nil {
224 return nil, fmt.Errorf("error reading manifest %q: %w", file, err)
225 }
226
227 manifest := sbox_proto.Manifest{}
228
Dan Willemsen4591b642021-05-24 14:24:12 -0700229 err = prototext.Unmarshal(manifestData, &manifest)
Colin Crosse16ce362020-11-12 08:29:30 -0800230 if err != nil {
231 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err)
232 }
233
234 return &manifest, nil
235}
236
237// runCommand runs a single command from a manifest. If the command references the
238// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700239func runCommand(command *sbox_proto.Command, tempDir string, commandIndex int) (depFile string, err error) {
Colin Crosse16ce362020-11-12 08:29:30 -0800240 rawCommand := command.GetCommand()
241 if rawCommand == "" {
242 return "", fmt.Errorf("command is required")
243 }
244
Colin Crosse52c2ac2022-03-28 17:03:35 -0700245 // Remove files from the output directory
246 err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged))
247 if err != nil {
248 return "", err
249 }
250
Colin Crosse55bd422021-03-23 13:44:30 -0700251 pathToTempDirInSbox := tempDir
252 if command.GetChdir() {
253 pathToTempDirInSbox = "."
254 }
255
Colin Crosse16ce362020-11-12 08:29:30 -0800256 err = os.MkdirAll(tempDir, 0777)
257 if err != nil {
258 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
259 }
260
261 // Copy in any files specified by the manifest.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700262 err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite)
Colin Crosse16ce362020-11-12 08:29:30 -0800263 if err != nil {
264 return "", err
265 }
Colin Crosse55bd422021-03-23 13:44:30 -0700266 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox)
267 if err != nil {
268 return "", err
Colin Crossc590ec42021-03-11 17:20:02 -0800269 }
270
Colin Crosse16ce362020-11-12 08:29:30 -0800271 if strings.Contains(rawCommand, depFilePlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800272 depFile = filepath.Join(pathToTempDirInSbox, "deps.d")
Colin Crosse16ce362020-11-12 08:29:30 -0800273 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
274 }
275
276 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800277 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1)
Colin Crosse16ce362020-11-12 08:29:30 -0800278 }
279
280 // Emulate ninja's behavior of creating the directories for any output files before
281 // running the command.
282 err = makeOutputDirs(command.CopyAfter, tempDir)
283 if err != nil {
284 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700285 }
286
Colin Cross40cade42022-04-04 18:16:04 -0700287 scriptName := fmt.Sprintf("sbox_command.%d.bash", commandIndex)
288 scriptPath := joinPath(tempDir, scriptName)
289 scriptPathInSandbox := joinPath(pathToTempDirInSbox, scriptName)
290 cmd, err := createCommandScript(rawCommand, scriptPath, scriptPathInSandbox)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700291 if err != nil {
292 return "", err
293 }
294
Colin Cross4258a392021-04-15 18:50:11 -0700295 buf := &bytes.Buffer{}
Jeff Gastonefc1b412017-03-29 17:29:06 -0700296 cmd.Stdin = os.Stdin
Colin Cross4258a392021-04-15 18:50:11 -0700297 cmd.Stdout = buf
298 cmd.Stderr = buf
Colin Crosse16ce362020-11-12 08:29:30 -0800299
300 if command.GetChdir() {
301 cmd.Dir = tempDir
Colin Crossc590ec42021-03-11 17:20:02 -0800302 path := os.Getenv("PATH")
303 absPath, err := makeAbsPathEnv(path)
304 if err != nil {
305 return "", err
306 }
307 err = os.Setenv("PATH", absPath)
308 if err != nil {
309 return "", fmt.Errorf("Failed to update PATH: %w", err)
310 }
Colin Crosse16ce362020-11-12 08:29:30 -0800311 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700312 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700313
Colin Crossfc2d8422021-04-15 18:36:42 -0700314 if err != nil {
315 // The command failed, do a best effort copy of output files out of the sandbox. This is
316 // especially useful for linters with baselines that print an error message on failure
317 // with a command to copy the output lint errors to the new baseline. Use a copy instead of
318 // a move to leave the sandbox intact for manual inspection
Colin Crosse52c2ac2022-03-28 17:03:35 -0700319 copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged))
Colin Crossfc2d8422021-04-15 18:36:42 -0700320 }
321
Colin Cross4258a392021-04-15 18:50:11 -0700322 // If the command was executed but failed with an error, print a debugging message before
323 // the command's output so it doesn't scroll the real error message off the screen.
Jeff Gastonefc1b412017-03-29 17:29:06 -0700324 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Cross4258a392021-04-15 18:50:11 -0700325 fmt.Fprintf(os.Stderr,
326 "The failing command was run inside an sbox sandbox in temporary directory\n"+
327 "%s\n"+
Colin Cross40cade42022-04-04 18:16:04 -0700328 "The failing command line can be found in\n"+
Colin Cross4258a392021-04-15 18:50:11 -0700329 "%s\n",
Colin Cross40cade42022-04-04 18:16:04 -0700330 tempDir, scriptPath)
Colin Cross4258a392021-04-15 18:50:11 -0700331 }
332
333 // Write the command's combined stdout/stderr.
334 os.Stdout.Write(buf.Bytes())
335
336 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800337 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700338 }
339
Colin Crosse52c2ac2022-03-28 17:03:35 -0700340 err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand)
341 if err != nil {
342 return "", err
Jeff Gastonf49082a2017-06-07 13:22:22 -0700343 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700344
Jeff Gastonf49082a2017-06-07 13:22:22 -0700345 // the created files match the declared files; now move them
Colin Crosse52c2ac2022-03-28 17:03:35 -0700346 err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged))
347 if err != nil {
348 return "", err
349 }
Colin Crosse16ce362020-11-12 08:29:30 -0800350
351 return depFile, nil
352}
353
354// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
355// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
356// so that the tools don't have to.
357func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
358 for _, copyPair := range copies {
359 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
360 err := os.MkdirAll(dir, 0777)
361 if err != nil {
362 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700363 }
Colin Crosse16ce362020-11-12 08:29:30 -0800364 }
365 return nil
366}
367
368// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
369// were created by the command.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700370func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800371 var missingOutputErrors []error
Colin Crosse52c2ac2022-03-28 17:03:35 -0700372 var incorrectOutputDirectoryErrors []error
Colin Crosse16ce362020-11-12 08:29:30 -0800373 for _, copyPair := range copies {
374 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
375 fileInfo, err := os.Stat(fromPath)
376 if err != nil {
377 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
378 continue
379 }
380 if fileInfo.IsDir() {
381 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
382 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700383
384 toPath := copyPair.GetTo()
385 if rel, err := filepath.Rel(outputDir, toPath); err != nil {
386 return err
387 } else if strings.HasPrefix(rel, "../") {
388 incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors,
389 fmt.Errorf("%s is not under %s", toPath, outputDir))
390 }
Colin Crosse16ce362020-11-12 08:29:30 -0800391 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700392
Steven Morelandee975ee2023-03-28 22:39:00 +0000393 const maxErrors = 25
Colin Crosse52c2ac2022-03-28 17:03:35 -0700394
395 if len(incorrectOutputDirectoryErrors) > 0 {
396 errorMessage := ""
397 more := 0
398 if len(incorrectOutputDirectoryErrors) > maxErrors {
399 more = len(incorrectOutputDirectoryErrors) - maxErrors
400 incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors]
401 }
402
403 for _, err := range incorrectOutputDirectoryErrors {
404 errorMessage += err.Error() + "\n"
405 }
406 if more > 0 {
407 errorMessage += fmt.Sprintf("...%v more", more)
408 }
409
410 return errors.New(errorMessage)
411 }
412
413 if len(missingOutputErrors) > 0 {
414 // find all created files for making a more informative error message
415 createdFiles := findAllFilesUnder(sandboxDir)
416
417 // build error message
418 errorMessage := "mismatch between declared and actual outputs\n"
419 errorMessage += "in sbox command(" + rawCommand + ")\n\n"
420 errorMessage += "in sandbox " + sandboxDir + ",\n"
421 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
422 for _, missingOutputError := range missingOutputErrors {
423 errorMessage += " " + missingOutputError.Error() + "\n"
424 }
425 if len(createdFiles) < 1 {
426 errorMessage += "created 0 files."
427 } else {
428 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
429 creationMessages := createdFiles
430 if len(creationMessages) > maxErrors {
431 creationMessages = creationMessages[:maxErrors]
432 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors))
433 }
434 for _, creationMessage := range creationMessages {
435 errorMessage += " " + creationMessage + "\n"
436 }
437 }
438
439 return errors.New(errorMessage)
440 }
441
442 return nil
Colin Crosse16ce362020-11-12 08:29:30 -0800443}
444
Colin Crosse52c2ac2022-03-28 17:03:35 -0700445type existsType bool
446
447const (
448 requireFromExists existsType = false
449 allowFromNotExists = true
450)
451
452type writeType bool
453
454const (
455 alwaysWrite writeType = false
456 onlyWriteIfChanged = true
457)
458
459// copyFiles copies files in or out of the sandbox. If exists is allowFromNotExists then errors
460// caused by a from path not existing are ignored. If write is onlyWriteIfChanged then the output
461// file is compared to the input file and not written to if it is the same, avoiding updating
462// the timestamp.
463func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800464 for _, copyPair := range copies {
465 fromPath := joinPath(fromDir, copyPair.GetFrom())
466 toPath := joinPath(toDir, copyPair.GetTo())
Colin Crosse52c2ac2022-03-28 17:03:35 -0700467 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write)
Colin Crosse16ce362020-11-12 08:29:30 -0800468 if err != nil {
469 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
470 }
471 }
472 return nil
473}
474
Colin Crossfc2d8422021-04-15 18:36:42 -0700475// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the
Colin Crosse52c2ac2022-03-28 17:03:35 -0700476// permissions. If exists is allowFromNotExists it returns nil if the from path doesn't exist.
477// If write is onlyWriteIfChanged then the output file is compared to the input file and not written to
478// if it is the same, avoiding updating the timestamp.
479func copyOneFile(from string, to string, forceExecutable bool, exists existsType,
480 write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800481 err := os.MkdirAll(filepath.Dir(to), 0777)
482 if err != nil {
483 return err
484 }
485
Colin Crosse16ce362020-11-12 08:29:30 -0800486 stat, err := os.Stat(from)
487 if err != nil {
Colin Crosse52c2ac2022-03-28 17:03:35 -0700488 if os.IsNotExist(err) && exists == allowFromNotExists {
Colin Crossfc2d8422021-04-15 18:36:42 -0700489 return nil
490 }
Colin Crosse16ce362020-11-12 08:29:30 -0800491 return err
492 }
493
494 perm := stat.Mode()
Colin Crossfc2d8422021-04-15 18:36:42 -0700495 if forceExecutable {
Colin Cross859dfd92020-11-30 20:12:47 -0800496 perm = perm | 0100 // u+x
497 }
Colin Crosse16ce362020-11-12 08:29:30 -0800498
Colin Crosse52c2ac2022-03-28 17:03:35 -0700499 if write == onlyWriteIfChanged && filesHaveSameContents(from, to) {
500 return nil
501 }
502
Colin Crosse16ce362020-11-12 08:29:30 -0800503 in, err := os.Open(from)
504 if err != nil {
505 return err
506 }
507 defer in.Close()
508
Colin Cross607c0b72021-03-31 12:54:06 -0700509 // Remove the target before copying. In most cases the file won't exist, but if there are
510 // duplicate copy rules for a file and the source file was read-only the second copy could
511 // fail.
512 err = os.Remove(to)
513 if err != nil && !os.IsNotExist(err) {
514 return err
515 }
516
Colin Crosse16ce362020-11-12 08:29:30 -0800517 out, err := os.Create(to)
518 if err != nil {
519 return err
520 }
521 defer func() {
522 out.Close()
523 if err != nil {
524 os.Remove(to)
525 }
526 }()
527
528 _, err = io.Copy(out, in)
529 if err != nil {
530 return err
531 }
532
533 if err = out.Close(); err != nil {
534 return err
535 }
536
537 if err = os.Chmod(to, perm); err != nil {
538 return err
539 }
540
541 return nil
542}
543
Colin Crosse55bd422021-03-23 13:44:30 -0700544// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files
545// listed into the sandbox.
546func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error {
547 for _, rspFile := range rspFiles {
548 err := copyOneRspFile(rspFile, toDir, toDirInSandbox)
549 if err != nil {
550 return err
551 }
552 }
553 return nil
554}
555
556// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files
557// listed into the sandbox.
558func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error {
559 in, err := os.Open(rspFile.GetFile())
560 if err != nil {
561 return err
562 }
563 defer in.Close()
564
565 files, err := response.ReadRspFile(in)
566 if err != nil {
567 return err
568 }
569
570 for i, from := range files {
571 // Convert the real path of the input file into the path inside the sandbox using the
572 // path mappings.
573 to := applyPathMappings(rspFile.PathMappings, from)
574
575 // Copy the file into the sandbox.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700576 err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite)
Colin Crosse55bd422021-03-23 13:44:30 -0700577 if err != nil {
578 return err
579 }
580
581 // Rewrite the name in the list of files to be relative to the sandbox directory.
582 files[i] = joinPath(toDirInSandbox, to)
583 }
584
585 // Convert the real path of the rsp file into the path inside the sandbox using the path
586 // mappings.
587 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile()))
588
589 err = os.MkdirAll(filepath.Dir(outRspFile), 0777)
590 if err != nil {
591 return err
592 }
593
594 out, err := os.Create(outRspFile)
595 if err != nil {
596 return err
597 }
598 defer out.Close()
599
600 // Write the rsp file with converted paths into the sandbox.
601 err = response.WriteRspFile(out, files)
602 if err != nil {
603 return err
604 }
605
606 return nil
607}
608
609// applyPathMappings takes a list of path mappings and a path, and returns the path with the first
610// matching path mapping applied. If the path does not match any of the path mappings then it is
611// returned unmodified.
612func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string {
613 for _, mapping := range pathMappings {
614 if strings.HasPrefix(path, mapping.GetFrom()+"/") {
615 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/"))
616 }
617 }
618 return path
619}
620
Colin Crosse16ce362020-11-12 08:29:30 -0800621// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
622// to moving files where the source and destination are in the same filesystem. This is OK for
Colin Crosse52c2ac2022-03-28 17:03:35 -0700623// sbox because the temporary directory is inside the out directory. If write is onlyWriteIfChanged
624// then the output file is compared to the input file and not written to if it is the same, avoiding
625// updating the timestamp. Otherwise it always updates the timestamp of the new file.
626func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800627 for _, copyPair := range copies {
628 fromPath := joinPath(fromDir, copyPair.GetFrom())
629 toPath := joinPath(toDir, copyPair.GetTo())
630 err := os.MkdirAll(filepath.Dir(toPath), 0777)
631 if err != nil {
632 return err
633 }
634
Colin Crosse52c2ac2022-03-28 17:03:35 -0700635 if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) {
636 continue
637 }
638
Colin Crosse16ce362020-11-12 08:29:30 -0800639 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800640 if err != nil {
641 return err
642 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700643
644 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
645 // files with old timestamps).
646 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800647 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700648 if err != nil {
649 return err
650 }
651 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700652 return nil
653}
Colin Crosse16ce362020-11-12 08:29:30 -0800654
Colin Crosse52c2ac2022-03-28 17:03:35 -0700655// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or
656// any files not listed in copies if write is onlyWriteIfChanged
657func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error {
658 if outputDir == "" {
659 return fmt.Errorf("output directory must be set")
660 }
661
662 if write == alwaysWrite {
663 // When writing all the output files remove the whole output directory
664 return os.RemoveAll(outputDir)
665 }
666
667 outputFiles := make(map[string]bool, len(copies))
668 for _, copyPair := range copies {
669 outputFiles[copyPair.GetTo()] = true
670 }
671
672 existingFiles := findAllFilesUnder(outputDir)
673 for _, existingFile := range existingFiles {
674 fullExistingFile := filepath.Join(outputDir, existingFile)
675 if !outputFiles[fullExistingFile] {
676 err := os.Remove(fullExistingFile)
677 if err != nil {
678 return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err)
679 }
680 }
681 }
682
683 return nil
684}
685
Colin Crosse16ce362020-11-12 08:29:30 -0800686// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
687// to an output file.
688func rewriteDepFiles(ins []string, out string) error {
689 var mergedDeps []string
690 for _, in := range ins {
691 data, err := ioutil.ReadFile(in)
692 if err != nil {
693 return err
694 }
695
696 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
697 if err != nil {
698 return err
699 }
700 mergedDeps = append(mergedDeps, deps.Inputs...)
701 }
702
703 deps := makedeps.Deps{
704 // Ninja doesn't care what the output file is, so we can use any string here.
705 Output: "outputfile",
706 Inputs: mergedDeps,
707 }
708
709 // Make the directory for the output depfile in case it is in a different directory
710 // than any of the output files.
711 outDir := filepath.Dir(out)
712 err := os.MkdirAll(outDir, 0777)
713 if err != nil {
714 return fmt.Errorf("failed to create %q: %w", outDir, err)
715 }
716
717 return ioutil.WriteFile(out, deps.Print(), 0666)
718}
719
720// joinPath wraps filepath.Join but returns file without appending to dir if file is
721// absolute.
722func joinPath(dir, file string) string {
723 if filepath.IsAbs(file) {
724 return file
725 }
726 return filepath.Join(dir, file)
727}
Colin Crossc590ec42021-03-11 17:20:02 -0800728
Colin Crosse52c2ac2022-03-28 17:03:35 -0700729// filesHaveSameContents compares the contents if two files, returning true if they are the same
730// and returning false if they are different or any errors occur.
731func filesHaveSameContents(a, b string) bool {
732 // Compare the sizes of the two files
733 statA, err := os.Stat(a)
734 if err != nil {
735 return false
736 }
737 statB, err := os.Stat(b)
738 if err != nil {
739 return false
740 }
741
742 if statA.Size() != statB.Size() {
743 return false
744 }
745
746 // Open the two files
747 fileA, err := os.Open(a)
748 if err != nil {
749 return false
750 }
751 defer fileA.Close()
Colin Crossfa8e9cc2022-04-12 17:26:58 -0700752 fileB, err := os.Open(b)
Colin Crosse52c2ac2022-03-28 17:03:35 -0700753 if err != nil {
754 return false
755 }
756 defer fileB.Close()
757
758 // Compare the files 1MB at a time
759 const bufSize = 1 * 1024 * 1024
760 bufA := make([]byte, bufSize)
761 bufB := make([]byte, bufSize)
762
763 remain := statA.Size()
764 for remain > 0 {
765 toRead := int64(bufSize)
766 if toRead > remain {
767 toRead = remain
768 }
769
770 _, err = io.ReadFull(fileA, bufA[:toRead])
771 if err != nil {
772 return false
773 }
774 _, err = io.ReadFull(fileB, bufB[:toRead])
775 if err != nil {
776 return false
777 }
778
779 if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 {
780 return false
781 }
782
783 remain -= toRead
784 }
785
786 return true
787}
788
Colin Crossc590ec42021-03-11 17:20:02 -0800789func makeAbsPathEnv(pathEnv string) (string, error) {
790 pathEnvElements := filepath.SplitList(pathEnv)
791 for i, p := range pathEnvElements {
792 if !filepath.IsAbs(p) {
793 absPath, err := filepath.Abs(p)
794 if err != nil {
795 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err)
796 }
797 pathEnvElements[i] = absPath
798 }
799 }
800 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil
801}