blob: 6459ea1757c0a7f4950ab9e5eddc32b2da1f3976 [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"
Cole Faustca355af2023-12-14 23:38:38 +000025 "io/fs"
Jeff Gastonefc1b412017-03-29 17:29:06 -070026 "io/ioutil"
27 "os"
28 "os/exec"
Jeff Gastonefc1b412017-03-29 17:29:06 -070029 "path/filepath"
Cole Faust1ead86c2024-08-23 14:41:51 -070030 "regexp"
Colin Crosse16ce362020-11-12 08:29:30 -080031 "strconv"
Jeff Gastonefc1b412017-03-29 17:29:06 -070032 "strings"
Colin Crossd1c1e6f2019-03-29 13:54:39 -070033 "time"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070034
Colin Crosse16ce362020-11-12 08:29:30 -080035 "android/soong/cmd/sbox/sbox_proto"
Dan Willemsenc89b6f12019-08-29 14:47:40 -070036 "android/soong/makedeps"
Colin Crosse55bd422021-03-23 13:44:30 -070037 "android/soong/response"
Colin Crosse16ce362020-11-12 08:29:30 -080038
Dan Willemsen4591b642021-05-24 14:24:12 -070039 "google.golang.org/protobuf/encoding/prototext"
Jeff Gastonefc1b412017-03-29 17:29:06 -070040)
41
Jeff Gaston93f0f372017-11-01 13:33:02 -070042var (
Colin Crosse52c2ac2022-03-28 17:03:35 -070043 sandboxesRoot string
44 outputDir string
45 manifestFile string
46 keepOutDir bool
47 writeIfChanged bool
Colin Crosse16ce362020-11-12 08:29:30 -080048)
49
50const (
51 depFilePlaceholder = "__SBOX_DEPFILE__"
52 sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__"
Jeff Gaston93f0f372017-11-01 13:33:02 -070053)
54
Cole Faust1ead86c2024-08-23 14:41:51 -070055var envVarNameRegex = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
56
Jeff Gaston93f0f372017-11-01 13:33:02 -070057func init() {
58 flag.StringVar(&sandboxesRoot, "sandbox-path", "",
59 "root of temp directory to put the sandbox into")
Colin Crosse52c2ac2022-03-28 17:03:35 -070060 flag.StringVar(&outputDir, "output-dir", "",
61 "directory which will contain all output files and only output files")
Colin Crosse16ce362020-11-12 08:29:30 -080062 flag.StringVar(&manifestFile, "manifest", "",
63 "textproto manifest describing the sandboxed command(s)")
Jeff Gaston93f0f372017-11-01 13:33:02 -070064 flag.BoolVar(&keepOutDir, "keep-out-dir", false,
65 "whether to keep the sandbox directory when done")
Colin Crosse52c2ac2022-03-28 17:03:35 -070066 flag.BoolVar(&writeIfChanged, "write-if-changed", false,
67 "only write the output files if they have changed")
Jeff Gaston93f0f372017-11-01 13:33:02 -070068}
69
70func usageViolation(violation string) {
71 if violation != "" {
72 fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation)
73 }
74
75 fmt.Fprintf(os.Stderr,
Colin Crosse16ce362020-11-12 08:29:30 -080076 "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n")
Jeff Gaston93f0f372017-11-01 13:33:02 -070077
78 flag.PrintDefaults()
79
80 os.Exit(1)
81}
82
Jeff Gastonefc1b412017-03-29 17:29:06 -070083func main() {
Jeff Gaston93f0f372017-11-01 13:33:02 -070084 flag.Usage = func() {
85 usageViolation("")
86 }
87 flag.Parse()
88
Jeff Gastonefc1b412017-03-29 17:29:06 -070089 error := run()
90 if error != nil {
91 fmt.Fprintln(os.Stderr, error)
92 os.Exit(1)
93 }
94}
95
Jeff Gaston90cfb092017-09-26 16:46:10 -070096func findAllFilesUnder(root string) (paths []string) {
97 paths = []string{}
98 filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
99 if !info.IsDir() {
100 relPath, err := filepath.Rel(root, path)
101 if err != nil {
102 // couldn't find relative path from ancestor?
103 panic(err)
104 }
105 paths = append(paths, relPath)
106 }
107 return nil
108 })
109 return paths
110}
111
Jeff Gastonefc1b412017-03-29 17:29:06 -0700112func run() error {
Colin Crosse16ce362020-11-12 08:29:30 -0800113 if manifestFile == "" {
114 usageViolation("--manifest <manifest> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700115 }
Jeff Gaston02a684b2017-10-27 14:59:27 -0700116 if sandboxesRoot == "" {
Jeff Gastonefc1b412017-03-29 17:29:06 -0700117 // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
118 // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
119 // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
120 // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
121 // and by passing it as a parameter we don't need to duplicate its value
Jeff Gaston93f0f372017-11-01 13:33:02 -0700122 usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty")
Jeff Gastonefc1b412017-03-29 17:29:06 -0700123 }
Colin Crosse16ce362020-11-12 08:29:30 -0800124
125 manifest, err := readManifest(manifestFile)
Sam Delmerico285b66a2023-09-25 12:13:17 +0000126 if err != nil {
127 return err
128 }
Colin Crosse16ce362020-11-12 08:29:30 -0800129
130 if len(manifest.Commands) == 0 {
131 return fmt.Errorf("at least one commands entry is required in %q", manifestFile)
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700132 }
133
Colin Crosse16ce362020-11-12 08:29:30 -0800134 // setup sandbox directory
135 err = os.MkdirAll(sandboxesRoot, 0777)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800136 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800137 return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800138 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700139
Ulf Adamsb73daa52020-11-25 23:09:09 +0100140 // This tool assumes that there are no two concurrent runs with the same
141 // manifestFile. It should therefore be safe to use the hash of the
142 // manifestFile as the temporary directory name. We do this because it
143 // makes the temporary directory name deterministic. There are some
144 // tools that embed the name of the temporary output in the output, and
145 // they otherwise cause non-determinism, which then poisons actions
146 // depending on this one.
147 hash := sha1.New()
148 hash.Write([]byte(manifestFile))
149 tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil)))
150
151 err = os.RemoveAll(tempDir)
152 if err != nil {
153 return err
154 }
155 err = os.MkdirAll(tempDir, 0777)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700156 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800157 return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700158 }
159
160 // In the common case, the following line of code is what removes the sandbox
161 // If a fatal error occurs (such as if our Go process is killed unexpectedly),
Colin Crosse16ce362020-11-12 08:29:30 -0800162 // then at the beginning of the next build, Soong will wipe the temporary
163 // directory.
Jeff Gastonf49082a2017-06-07 13:22:22 -0700164 defer func() {
165 // in some cases we decline to remove the temp dir, to facilitate debugging
Jeff Gaston93f0f372017-11-01 13:33:02 -0700166 if !keepOutDir {
Jeff Gastonf49082a2017-06-07 13:22:22 -0700167 os.RemoveAll(tempDir)
168 }
169 }()
Jeff Gastonefc1b412017-03-29 17:29:06 -0700170
Colin Crosse16ce362020-11-12 08:29:30 -0800171 // If there is more than one command in the manifest use a separate directory for each one.
172 useSubDir := len(manifest.Commands) > 1
173 var commandDepFiles []string
Jeff Gastonefc1b412017-03-29 17:29:06 -0700174
Colin Crosse16ce362020-11-12 08:29:30 -0800175 for i, command := range manifest.Commands {
176 localTempDir := tempDir
177 if useSubDir {
178 localTempDir = filepath.Join(localTempDir, strconv.Itoa(i))
Jeff Gastonefc1b412017-03-29 17:29:06 -0700179 }
Kevin DuBois0a5da702021-10-05 15:41:02 -0700180 depFile, err := runCommand(command, localTempDir, i)
Jeff Gaston02a684b2017-10-27 14:59:27 -0700181 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800182 // Running the command failed, keep the temporary output directory around in
183 // case a user wants to inspect it for debugging purposes. Soong will delete
184 // it at the beginning of the next build anyway.
185 keepOutDir = true
Jeff Gaston02a684b2017-10-27 14:59:27 -0700186 return err
187 }
Colin Crosse16ce362020-11-12 08:29:30 -0800188 if depFile != "" {
189 commandDepFiles = append(commandDepFiles, depFile)
190 }
191 }
192
193 outputDepFile := manifest.GetOutputDepfile()
194 if len(commandDepFiles) > 0 && outputDepFile == "" {
195 return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file",
196 depFilePlaceholder)
197 }
198
199 if outputDepFile != "" {
200 // Merge the depfiles from each command in the manifest to a single output depfile.
201 err = rewriteDepFiles(commandDepFiles, outputDepFile)
202 if err != nil {
203 return fmt.Errorf("failed merging depfiles: %w", err)
204 }
205 }
206
207 return nil
208}
209
Kevin DuBois0a5da702021-10-05 15:41:02 -0700210// createCommandScript will create and return an exec.Cmd that runs rawCommand.
211//
212// rawCommand is executed via a script in the sandbox.
Colin Cross40cade42022-04-04 18:16:04 -0700213// scriptPath is the temporary where the script is created.
214// scriptPathInSandbox is the path to the script in the sbox environment.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700215//
216// returns an exec.Cmd that can be ran from within sbox context if no error, or nil if error.
217// caller must ensure script is cleaned up if function succeeds.
Colin Cross40cade42022-04-04 18:16:04 -0700218func createCommandScript(rawCommand, scriptPath, scriptPathInSandbox string) (*exec.Cmd, error) {
219 err := os.WriteFile(scriptPath, []byte(rawCommand), 0644)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700220 if err != nil {
221 return nil, fmt.Errorf("failed to write command %s... to %s",
Colin Cross40cade42022-04-04 18:16:04 -0700222 rawCommand[0:40], scriptPath)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700223 }
Colin Cross40cade42022-04-04 18:16:04 -0700224 return exec.Command("bash", scriptPathInSandbox), nil
Kevin DuBois0a5da702021-10-05 15:41:02 -0700225}
226
Colin Crosse16ce362020-11-12 08:29:30 -0800227// readManifest reads an sbox manifest from a textproto file.
228func readManifest(file string) (*sbox_proto.Manifest, error) {
229 manifestData, err := ioutil.ReadFile(file)
230 if err != nil {
231 return nil, fmt.Errorf("error reading manifest %q: %w", file, err)
232 }
233
234 manifest := sbox_proto.Manifest{}
235
Dan Willemsen4591b642021-05-24 14:24:12 -0700236 err = prototext.Unmarshal(manifestData, &manifest)
Colin Crosse16ce362020-11-12 08:29:30 -0800237 if err != nil {
238 return nil, fmt.Errorf("error parsing manifest %q: %w", file, err)
239 }
240
241 return &manifest, nil
242}
243
Cole Faust1ead86c2024-08-23 14:41:51 -0700244func createEnv(command *sbox_proto.Command) ([]string, error) {
245 env := []string{}
246 if command.DontInheritEnv == nil || !*command.DontInheritEnv {
247 env = os.Environ()
248 }
249 for _, envVar := range command.Env {
250 if envVar.Name == nil || !envVarNameRegex.MatchString(*envVar.Name) {
251 name := "nil"
252 if envVar.Name != nil {
253 name = *envVar.Name
254 }
255 return nil, fmt.Errorf("Invalid environment variable name: %q", name)
256 }
257 if envVar.State == nil {
258 return nil, fmt.Errorf("Must set state")
259 }
260 switch state := envVar.State.(type) {
261 case *sbox_proto.EnvironmentVariable_Value:
262 env = append(env, *envVar.Name+"="+state.Value)
263 case *sbox_proto.EnvironmentVariable_Unset:
264 if !state.Unset {
265 return nil, fmt.Errorf("Can't have unset set to false")
266 }
267 prefix := *envVar.Name + "="
268 for i := 0; i < len(env); i++ {
269 if strings.HasPrefix(env[i], prefix) {
270 env = append(env[:i], env[i+1:]...)
271 i--
272 }
273 }
274 case *sbox_proto.EnvironmentVariable_Inherit:
275 if !state.Inherit {
276 return nil, fmt.Errorf("Can't have inherit set to false")
277 }
278 env = append(env, *envVar.Name+"="+os.Getenv(*envVar.Name))
279 default:
280 return nil, fmt.Errorf("Unhandled state type")
281 }
282 }
283 return env, nil
284}
285
Colin Crosse16ce362020-11-12 08:29:30 -0800286// runCommand runs a single command from a manifest. If the command references the
287// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700288func runCommand(command *sbox_proto.Command, tempDir string, commandIndex int) (depFile string, err error) {
Colin Crosse16ce362020-11-12 08:29:30 -0800289 rawCommand := command.GetCommand()
290 if rawCommand == "" {
291 return "", fmt.Errorf("command is required")
292 }
293
Colin Crosse52c2ac2022-03-28 17:03:35 -0700294 // Remove files from the output directory
295 err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged))
296 if err != nil {
297 return "", err
298 }
299
Colin Crosse55bd422021-03-23 13:44:30 -0700300 pathToTempDirInSbox := tempDir
301 if command.GetChdir() {
302 pathToTempDirInSbox = "."
303 }
304
Colin Crosse16ce362020-11-12 08:29:30 -0800305 err = os.MkdirAll(tempDir, 0777)
306 if err != nil {
307 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
308 }
309
310 // Copy in any files specified by the manifest.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700311 err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite)
Colin Crosse16ce362020-11-12 08:29:30 -0800312 if err != nil {
313 return "", err
314 }
Colin Crosse55bd422021-03-23 13:44:30 -0700315 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox)
316 if err != nil {
317 return "", err
Colin Crossc590ec42021-03-11 17:20:02 -0800318 }
319
Colin Crosse16ce362020-11-12 08:29:30 -0800320 if strings.Contains(rawCommand, depFilePlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800321 depFile = filepath.Join(pathToTempDirInSbox, "deps.d")
Colin Crosse16ce362020-11-12 08:29:30 -0800322 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
323 }
324
325 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800326 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1)
Colin Crosse16ce362020-11-12 08:29:30 -0800327 }
328
329 // Emulate ninja's behavior of creating the directories for any output files before
330 // running the command.
331 err = makeOutputDirs(command.CopyAfter, tempDir)
332 if err != nil {
333 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700334 }
335
Colin Cross40cade42022-04-04 18:16:04 -0700336 scriptName := fmt.Sprintf("sbox_command.%d.bash", commandIndex)
337 scriptPath := joinPath(tempDir, scriptName)
338 scriptPathInSandbox := joinPath(pathToTempDirInSbox, scriptName)
339 cmd, err := createCommandScript(rawCommand, scriptPath, scriptPathInSandbox)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700340 if err != nil {
341 return "", err
342 }
343
Colin Cross4258a392021-04-15 18:50:11 -0700344 buf := &bytes.Buffer{}
Jeff Gastonefc1b412017-03-29 17:29:06 -0700345 cmd.Stdin = os.Stdin
Colin Cross4258a392021-04-15 18:50:11 -0700346 cmd.Stdout = buf
347 cmd.Stderr = buf
Colin Crosse16ce362020-11-12 08:29:30 -0800348
349 if command.GetChdir() {
350 cmd.Dir = tempDir
Colin Crossc590ec42021-03-11 17:20:02 -0800351 path := os.Getenv("PATH")
352 absPath, err := makeAbsPathEnv(path)
353 if err != nil {
354 return "", err
355 }
356 err = os.Setenv("PATH", absPath)
357 if err != nil {
358 return "", fmt.Errorf("Failed to update PATH: %w", err)
359 }
Colin Crosse16ce362020-11-12 08:29:30 -0800360 }
Cole Faust1ead86c2024-08-23 14:41:51 -0700361
362 cmd.Env, err = createEnv(command)
363 if err != nil {
364 return "", err
365 }
366
Jeff Gastonefc1b412017-03-29 17:29:06 -0700367 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700368
Colin Crossfc2d8422021-04-15 18:36:42 -0700369 if err != nil {
370 // The command failed, do a best effort copy of output files out of the sandbox. This is
371 // especially useful for linters with baselines that print an error message on failure
372 // with a command to copy the output lint errors to the new baseline. Use a copy instead of
373 // a move to leave the sandbox intact for manual inspection
Colin Crosse52c2ac2022-03-28 17:03:35 -0700374 copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged))
Colin Crossfc2d8422021-04-15 18:36:42 -0700375 }
376
Colin Cross4258a392021-04-15 18:50:11 -0700377 // If the command was executed but failed with an error, print a debugging message before
378 // the command's output so it doesn't scroll the real error message off the screen.
Jeff Gastonefc1b412017-03-29 17:29:06 -0700379 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Cross4258a392021-04-15 18:50:11 -0700380 fmt.Fprintf(os.Stderr,
381 "The failing command was run inside an sbox sandbox in temporary directory\n"+
382 "%s\n"+
Colin Cross40cade42022-04-04 18:16:04 -0700383 "The failing command line can be found in\n"+
Colin Cross4258a392021-04-15 18:50:11 -0700384 "%s\n",
Colin Cross40cade42022-04-04 18:16:04 -0700385 tempDir, scriptPath)
Colin Cross4258a392021-04-15 18:50:11 -0700386 }
387
388 // Write the command's combined stdout/stderr.
389 os.Stdout.Write(buf.Bytes())
390
391 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800392 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700393 }
394
Colin Crosse52c2ac2022-03-28 17:03:35 -0700395 err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand)
396 if err != nil {
397 return "", err
Jeff Gastonf49082a2017-06-07 13:22:22 -0700398 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700399
Jeff Gastonf49082a2017-06-07 13:22:22 -0700400 // the created files match the declared files; now move them
Colin Crosse52c2ac2022-03-28 17:03:35 -0700401 err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged))
402 if err != nil {
403 return "", err
404 }
Colin Crosse16ce362020-11-12 08:29:30 -0800405
406 return depFile, nil
407}
408
409// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
410// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
411// so that the tools don't have to.
412func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
413 for _, copyPair := range copies {
414 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
415 err := os.MkdirAll(dir, 0777)
416 if err != nil {
417 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700418 }
Colin Crosse16ce362020-11-12 08:29:30 -0800419 }
420 return nil
421}
422
423// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
424// were created by the command.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700425func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800426 var missingOutputErrors []error
Colin Crosse52c2ac2022-03-28 17:03:35 -0700427 var incorrectOutputDirectoryErrors []error
Colin Crosse16ce362020-11-12 08:29:30 -0800428 for _, copyPair := range copies {
429 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
430 fileInfo, err := os.Stat(fromPath)
431 if err != nil {
432 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
433 continue
434 }
435 if fileInfo.IsDir() {
436 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
437 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700438
439 toPath := copyPair.GetTo()
440 if rel, err := filepath.Rel(outputDir, toPath); err != nil {
441 return err
442 } else if strings.HasPrefix(rel, "../") {
443 incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors,
444 fmt.Errorf("%s is not under %s", toPath, outputDir))
445 }
Colin Crosse16ce362020-11-12 08:29:30 -0800446 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700447
Steven Morelandee975ee2023-03-28 22:39:00 +0000448 const maxErrors = 25
Colin Crosse52c2ac2022-03-28 17:03:35 -0700449
450 if len(incorrectOutputDirectoryErrors) > 0 {
451 errorMessage := ""
452 more := 0
453 if len(incorrectOutputDirectoryErrors) > maxErrors {
454 more = len(incorrectOutputDirectoryErrors) - maxErrors
455 incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors]
456 }
457
458 for _, err := range incorrectOutputDirectoryErrors {
459 errorMessage += err.Error() + "\n"
460 }
461 if more > 0 {
462 errorMessage += fmt.Sprintf("...%v more", more)
463 }
464
465 return errors.New(errorMessage)
466 }
467
468 if len(missingOutputErrors) > 0 {
469 // find all created files for making a more informative error message
470 createdFiles := findAllFilesUnder(sandboxDir)
471
472 // build error message
473 errorMessage := "mismatch between declared and actual outputs\n"
474 errorMessage += "in sbox command(" + rawCommand + ")\n\n"
475 errorMessage += "in sandbox " + sandboxDir + ",\n"
476 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
477 for _, missingOutputError := range missingOutputErrors {
478 errorMessage += " " + missingOutputError.Error() + "\n"
479 }
480 if len(createdFiles) < 1 {
481 errorMessage += "created 0 files."
482 } else {
483 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
484 creationMessages := createdFiles
485 if len(creationMessages) > maxErrors {
486 creationMessages = creationMessages[:maxErrors]
487 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors))
488 }
489 for _, creationMessage := range creationMessages {
490 errorMessage += " " + creationMessage + "\n"
491 }
492 }
493
494 return errors.New(errorMessage)
495 }
496
497 return nil
Colin Crosse16ce362020-11-12 08:29:30 -0800498}
499
Colin Crosse52c2ac2022-03-28 17:03:35 -0700500type existsType bool
501
502const (
503 requireFromExists existsType = false
504 allowFromNotExists = true
505)
506
507type writeType bool
508
509const (
510 alwaysWrite writeType = false
511 onlyWriteIfChanged = true
512)
513
514// copyFiles copies files in or out of the sandbox. If exists is allowFromNotExists then errors
515// caused by a from path not existing are ignored. If write is onlyWriteIfChanged then the output
516// file is compared to the input file and not written to if it is the same, avoiding updating
517// the timestamp.
518func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800519 for _, copyPair := range copies {
520 fromPath := joinPath(fromDir, copyPair.GetFrom())
521 toPath := joinPath(toDir, copyPair.GetTo())
Colin Crosse52c2ac2022-03-28 17:03:35 -0700522 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write)
Colin Crosse16ce362020-11-12 08:29:30 -0800523 if err != nil {
524 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
525 }
526 }
527 return nil
528}
529
Colin Crossfc2d8422021-04-15 18:36:42 -0700530// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the
Colin Crosse52c2ac2022-03-28 17:03:35 -0700531// permissions. If exists is allowFromNotExists it returns nil if the from path doesn't exist.
532// If write is onlyWriteIfChanged then the output file is compared to the input file and not written to
Cole Faustca355af2023-12-14 23:38:38 +0000533// if it is the same, avoiding updating the timestamp. If from is a symlink, the symlink itself
534// will be copied, instead of what it points to.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700535func copyOneFile(from string, to string, forceExecutable bool, exists existsType,
536 write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800537 err := os.MkdirAll(filepath.Dir(to), 0777)
538 if err != nil {
539 return err
540 }
541
Cole Faustca355af2023-12-14 23:38:38 +0000542 stat, err := os.Lstat(from)
Colin Crosse16ce362020-11-12 08:29:30 -0800543 if err != nil {
Colin Crosse52c2ac2022-03-28 17:03:35 -0700544 if os.IsNotExist(err) && exists == allowFromNotExists {
Colin Crossfc2d8422021-04-15 18:36:42 -0700545 return nil
546 }
Colin Crosse16ce362020-11-12 08:29:30 -0800547 return err
548 }
549
Cole Faustca355af2023-12-14 23:38:38 +0000550 if stat.Mode()&fs.ModeSymlink != 0 {
551 linkTarget, err := os.Readlink(from)
552 if err != nil {
553 return err
554 }
555 if write == onlyWriteIfChanged {
556 toLinkTarget, err := os.Readlink(to)
557 if err == nil && toLinkTarget == linkTarget {
558 return nil
559 }
560 }
561 err = os.Remove(to)
562 if err != nil && !os.IsNotExist(err) {
563 return err
564 }
565
566 return os.Symlink(linkTarget, to)
567 }
568
Colin Crosse16ce362020-11-12 08:29:30 -0800569 perm := stat.Mode()
Colin Crossfc2d8422021-04-15 18:36:42 -0700570 if forceExecutable {
Colin Cross859dfd92020-11-30 20:12:47 -0800571 perm = perm | 0100 // u+x
572 }
Colin Crosse16ce362020-11-12 08:29:30 -0800573
Colin Crosse52c2ac2022-03-28 17:03:35 -0700574 if write == onlyWriteIfChanged && filesHaveSameContents(from, to) {
575 return nil
576 }
577
Colin Crosse16ce362020-11-12 08:29:30 -0800578 in, err := os.Open(from)
579 if err != nil {
580 return err
581 }
582 defer in.Close()
583
Colin Cross607c0b72021-03-31 12:54:06 -0700584 // Remove the target before copying. In most cases the file won't exist, but if there are
585 // duplicate copy rules for a file and the source file was read-only the second copy could
586 // fail.
587 err = os.Remove(to)
588 if err != nil && !os.IsNotExist(err) {
589 return err
590 }
591
Colin Crosse16ce362020-11-12 08:29:30 -0800592 out, err := os.Create(to)
593 if err != nil {
594 return err
595 }
596 defer func() {
597 out.Close()
598 if err != nil {
599 os.Remove(to)
600 }
601 }()
602
603 _, err = io.Copy(out, in)
604 if err != nil {
605 return err
606 }
607
608 if err = out.Close(); err != nil {
609 return err
610 }
611
612 if err = os.Chmod(to, perm); err != nil {
613 return err
614 }
615
616 return nil
617}
618
Colin Crosse55bd422021-03-23 13:44:30 -0700619// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files
620// listed into the sandbox.
621func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error {
622 for _, rspFile := range rspFiles {
623 err := copyOneRspFile(rspFile, toDir, toDirInSandbox)
624 if err != nil {
625 return err
626 }
627 }
628 return nil
629}
630
631// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files
632// listed into the sandbox.
633func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error {
634 in, err := os.Open(rspFile.GetFile())
635 if err != nil {
636 return err
637 }
638 defer in.Close()
639
640 files, err := response.ReadRspFile(in)
641 if err != nil {
642 return err
643 }
644
645 for i, from := range files {
646 // Convert the real path of the input file into the path inside the sandbox using the
647 // path mappings.
648 to := applyPathMappings(rspFile.PathMappings, from)
649
650 // Copy the file into the sandbox.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700651 err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite)
Colin Crosse55bd422021-03-23 13:44:30 -0700652 if err != nil {
653 return err
654 }
655
656 // Rewrite the name in the list of files to be relative to the sandbox directory.
657 files[i] = joinPath(toDirInSandbox, to)
658 }
659
660 // Convert the real path of the rsp file into the path inside the sandbox using the path
661 // mappings.
662 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile()))
663
664 err = os.MkdirAll(filepath.Dir(outRspFile), 0777)
665 if err != nil {
666 return err
667 }
668
669 out, err := os.Create(outRspFile)
670 if err != nil {
671 return err
672 }
673 defer out.Close()
674
675 // Write the rsp file with converted paths into the sandbox.
676 err = response.WriteRspFile(out, files)
677 if err != nil {
678 return err
679 }
680
681 return nil
682}
683
684// applyPathMappings takes a list of path mappings and a path, and returns the path with the first
685// matching path mapping applied. If the path does not match any of the path mappings then it is
686// returned unmodified.
687func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string {
688 for _, mapping := range pathMappings {
689 if strings.HasPrefix(path, mapping.GetFrom()+"/") {
690 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/"))
691 }
692 }
693 return path
694}
695
Colin Crosse16ce362020-11-12 08:29:30 -0800696// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
697// to moving files where the source and destination are in the same filesystem. This is OK for
Colin Crosse52c2ac2022-03-28 17:03:35 -0700698// sbox because the temporary directory is inside the out directory. If write is onlyWriteIfChanged
699// then the output file is compared to the input file and not written to if it is the same, avoiding
700// updating the timestamp. Otherwise it always updates the timestamp of the new file.
701func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800702 for _, copyPair := range copies {
703 fromPath := joinPath(fromDir, copyPair.GetFrom())
704 toPath := joinPath(toDir, copyPair.GetTo())
705 err := os.MkdirAll(filepath.Dir(toPath), 0777)
706 if err != nil {
707 return err
708 }
709
Colin Crosse52c2ac2022-03-28 17:03:35 -0700710 if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) {
711 continue
712 }
713
Colin Crosse16ce362020-11-12 08:29:30 -0800714 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800715 if err != nil {
716 return err
717 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700718
719 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
720 // files with old timestamps).
721 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800722 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700723 if err != nil {
724 return err
725 }
726 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700727 return nil
728}
Colin Crosse16ce362020-11-12 08:29:30 -0800729
Colin Crosse52c2ac2022-03-28 17:03:35 -0700730// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or
731// any files not listed in copies if write is onlyWriteIfChanged
732func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error {
733 if outputDir == "" {
734 return fmt.Errorf("output directory must be set")
735 }
736
737 if write == alwaysWrite {
738 // When writing all the output files remove the whole output directory
739 return os.RemoveAll(outputDir)
740 }
741
742 outputFiles := make(map[string]bool, len(copies))
743 for _, copyPair := range copies {
744 outputFiles[copyPair.GetTo()] = true
745 }
746
747 existingFiles := findAllFilesUnder(outputDir)
748 for _, existingFile := range existingFiles {
749 fullExistingFile := filepath.Join(outputDir, existingFile)
750 if !outputFiles[fullExistingFile] {
751 err := os.Remove(fullExistingFile)
752 if err != nil {
753 return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err)
754 }
755 }
756 }
757
758 return nil
759}
760
Colin Crosse16ce362020-11-12 08:29:30 -0800761// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
762// to an output file.
763func rewriteDepFiles(ins []string, out string) error {
764 var mergedDeps []string
765 for _, in := range ins {
766 data, err := ioutil.ReadFile(in)
767 if err != nil {
768 return err
769 }
770
771 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
772 if err != nil {
773 return err
774 }
775 mergedDeps = append(mergedDeps, deps.Inputs...)
776 }
777
778 deps := makedeps.Deps{
779 // Ninja doesn't care what the output file is, so we can use any string here.
780 Output: "outputfile",
781 Inputs: mergedDeps,
782 }
783
784 // Make the directory for the output depfile in case it is in a different directory
785 // than any of the output files.
786 outDir := filepath.Dir(out)
787 err := os.MkdirAll(outDir, 0777)
788 if err != nil {
789 return fmt.Errorf("failed to create %q: %w", outDir, err)
790 }
791
792 return ioutil.WriteFile(out, deps.Print(), 0666)
793}
794
795// joinPath wraps filepath.Join but returns file without appending to dir if file is
796// absolute.
797func joinPath(dir, file string) string {
798 if filepath.IsAbs(file) {
799 return file
800 }
801 return filepath.Join(dir, file)
802}
Colin Crossc590ec42021-03-11 17:20:02 -0800803
Colin Crosse52c2ac2022-03-28 17:03:35 -0700804// filesHaveSameContents compares the contents if two files, returning true if they are the same
805// and returning false if they are different or any errors occur.
806func filesHaveSameContents(a, b string) bool {
807 // Compare the sizes of the two files
808 statA, err := os.Stat(a)
809 if err != nil {
810 return false
811 }
812 statB, err := os.Stat(b)
813 if err != nil {
814 return false
815 }
816
817 if statA.Size() != statB.Size() {
818 return false
819 }
820
821 // Open the two files
822 fileA, err := os.Open(a)
823 if err != nil {
824 return false
825 }
826 defer fileA.Close()
Colin Crossfa8e9cc2022-04-12 17:26:58 -0700827 fileB, err := os.Open(b)
Colin Crosse52c2ac2022-03-28 17:03:35 -0700828 if err != nil {
829 return false
830 }
831 defer fileB.Close()
832
833 // Compare the files 1MB at a time
834 const bufSize = 1 * 1024 * 1024
835 bufA := make([]byte, bufSize)
836 bufB := make([]byte, bufSize)
837
838 remain := statA.Size()
839 for remain > 0 {
840 toRead := int64(bufSize)
841 if toRead > remain {
842 toRead = remain
843 }
844
845 _, err = io.ReadFull(fileA, bufA[:toRead])
846 if err != nil {
847 return false
848 }
849 _, err = io.ReadFull(fileB, bufB[:toRead])
850 if err != nil {
851 return false
852 }
853
854 if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 {
855 return false
856 }
857
858 remain -= toRead
859 }
860
861 return true
862}
863
Colin Crossc590ec42021-03-11 17:20:02 -0800864func makeAbsPathEnv(pathEnv string) (string, error) {
865 pathEnvElements := filepath.SplitList(pathEnv)
866 for i, p := range pathEnvElements {
867 if !filepath.IsAbs(p) {
868 absPath, err := filepath.Abs(p)
869 if err != nil {
870 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err)
871 }
872 pathEnvElements[i] = absPath
873 }
874 }
875 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil
876}