blob: f3931a45e272a77e9fa4148cf61fc4768a177d15 [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 }
Cole Faust63ea1f92024-08-27 11:42:26 -0700278 val, ok := os.LookupEnv(*envVar.Name)
279 if ok {
280 env = append(env, *envVar.Name+"="+val)
281 }
Cole Faust1ead86c2024-08-23 14:41:51 -0700282 default:
283 return nil, fmt.Errorf("Unhandled state type")
284 }
285 }
286 return env, nil
287}
288
Colin Crosse16ce362020-11-12 08:29:30 -0800289// runCommand runs a single command from a manifest. If the command references the
290// __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used.
Kevin DuBois0a5da702021-10-05 15:41:02 -0700291func runCommand(command *sbox_proto.Command, tempDir string, commandIndex int) (depFile string, err error) {
Colin Crosse16ce362020-11-12 08:29:30 -0800292 rawCommand := command.GetCommand()
293 if rawCommand == "" {
294 return "", fmt.Errorf("command is required")
295 }
296
Colin Crosse52c2ac2022-03-28 17:03:35 -0700297 // Remove files from the output directory
298 err = clearOutputDirectory(command.CopyAfter, outputDir, writeType(writeIfChanged))
299 if err != nil {
300 return "", err
301 }
302
Colin Crosse55bd422021-03-23 13:44:30 -0700303 pathToTempDirInSbox := tempDir
304 if command.GetChdir() {
305 pathToTempDirInSbox = "."
306 }
307
Colin Crosse16ce362020-11-12 08:29:30 -0800308 err = os.MkdirAll(tempDir, 0777)
309 if err != nil {
310 return "", fmt.Errorf("failed to create %q: %w", tempDir, err)
311 }
312
313 // Copy in any files specified by the manifest.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700314 err = copyFiles(command.CopyBefore, "", tempDir, requireFromExists, alwaysWrite)
Colin Crosse16ce362020-11-12 08:29:30 -0800315 if err != nil {
316 return "", err
317 }
Colin Crosse55bd422021-03-23 13:44:30 -0700318 err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox)
319 if err != nil {
320 return "", err
Colin Crossc590ec42021-03-11 17:20:02 -0800321 }
322
Colin Crosse16ce362020-11-12 08:29:30 -0800323 if strings.Contains(rawCommand, depFilePlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800324 depFile = filepath.Join(pathToTempDirInSbox, "deps.d")
Colin Crosse16ce362020-11-12 08:29:30 -0800325 rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1)
326 }
327
328 if strings.Contains(rawCommand, sandboxDirPlaceholder) {
Colin Crossc590ec42021-03-11 17:20:02 -0800329 rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1)
Colin Crosse16ce362020-11-12 08:29:30 -0800330 }
331
332 // Emulate ninja's behavior of creating the directories for any output files before
333 // running the command.
334 err = makeOutputDirs(command.CopyAfter, tempDir)
335 if err != nil {
336 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700337 }
338
Colin Cross40cade42022-04-04 18:16:04 -0700339 scriptName := fmt.Sprintf("sbox_command.%d.bash", commandIndex)
340 scriptPath := joinPath(tempDir, scriptName)
341 scriptPathInSandbox := joinPath(pathToTempDirInSbox, scriptName)
342 cmd, err := createCommandScript(rawCommand, scriptPath, scriptPathInSandbox)
Kevin DuBois0a5da702021-10-05 15:41:02 -0700343 if err != nil {
344 return "", err
345 }
346
Colin Cross4258a392021-04-15 18:50:11 -0700347 buf := &bytes.Buffer{}
Jeff Gastonefc1b412017-03-29 17:29:06 -0700348 cmd.Stdin = os.Stdin
Colin Cross4258a392021-04-15 18:50:11 -0700349 cmd.Stdout = buf
350 cmd.Stderr = buf
Colin Crosse16ce362020-11-12 08:29:30 -0800351
352 if command.GetChdir() {
353 cmd.Dir = tempDir
Colin Crossc590ec42021-03-11 17:20:02 -0800354 path := os.Getenv("PATH")
355 absPath, err := makeAbsPathEnv(path)
356 if err != nil {
357 return "", err
358 }
359 err = os.Setenv("PATH", absPath)
360 if err != nil {
361 return "", fmt.Errorf("Failed to update PATH: %w", err)
362 }
Colin Crosse16ce362020-11-12 08:29:30 -0800363 }
Cole Faust1ead86c2024-08-23 14:41:51 -0700364
365 cmd.Env, err = createEnv(command)
366 if err != nil {
367 return "", err
368 }
369
Jeff Gastonefc1b412017-03-29 17:29:06 -0700370 err = cmd.Run()
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700371
Colin Crossfc2d8422021-04-15 18:36:42 -0700372 if err != nil {
373 // The command failed, do a best effort copy of output files out of the sandbox. This is
374 // especially useful for linters with baselines that print an error message on failure
375 // with a command to copy the output lint errors to the new baseline. Use a copy instead of
376 // a move to leave the sandbox intact for manual inspection
Colin Crosse52c2ac2022-03-28 17:03:35 -0700377 copyFiles(command.CopyAfter, tempDir, "", allowFromNotExists, writeType(writeIfChanged))
Colin Crossfc2d8422021-04-15 18:36:42 -0700378 }
379
Colin Cross4258a392021-04-15 18:50:11 -0700380 // If the command was executed but failed with an error, print a debugging message before
381 // the command's output so it doesn't scroll the real error message off the screen.
Jeff Gastonefc1b412017-03-29 17:29:06 -0700382 if exit, ok := err.(*exec.ExitError); ok && !exit.Success() {
Colin Cross4258a392021-04-15 18:50:11 -0700383 fmt.Fprintf(os.Stderr,
384 "The failing command was run inside an sbox sandbox in temporary directory\n"+
385 "%s\n"+
Colin Cross40cade42022-04-04 18:16:04 -0700386 "The failing command line can be found in\n"+
Colin Cross4258a392021-04-15 18:50:11 -0700387 "%s\n",
Colin Cross40cade42022-04-04 18:16:04 -0700388 tempDir, scriptPath)
Colin Cross4258a392021-04-15 18:50:11 -0700389 }
390
391 // Write the command's combined stdout/stderr.
392 os.Stdout.Write(buf.Bytes())
393
394 if err != nil {
Colin Crosse16ce362020-11-12 08:29:30 -0800395 return "", err
Jeff Gastonefc1b412017-03-29 17:29:06 -0700396 }
397
Colin Crosse52c2ac2022-03-28 17:03:35 -0700398 err = validateOutputFiles(command.CopyAfter, tempDir, outputDir, rawCommand)
399 if err != nil {
400 return "", err
Jeff Gastonf49082a2017-06-07 13:22:22 -0700401 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700402
Jeff Gastonf49082a2017-06-07 13:22:22 -0700403 // the created files match the declared files; now move them
Colin Crosse52c2ac2022-03-28 17:03:35 -0700404 err = moveFiles(command.CopyAfter, tempDir, "", writeType(writeIfChanged))
405 if err != nil {
406 return "", err
407 }
Colin Crosse16ce362020-11-12 08:29:30 -0800408
409 return depFile, nil
410}
411
412// makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied
413// out of the sandbox. This emulate's Ninja's behavior of creating directories for output files
414// so that the tools don't have to.
415func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error {
416 for _, copyPair := range copies {
417 dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom()))
418 err := os.MkdirAll(dir, 0777)
419 if err != nil {
420 return err
Jeff Gaston193f2fb2017-06-12 15:00:12 -0700421 }
Colin Crosse16ce362020-11-12 08:29:30 -0800422 }
423 return nil
424}
425
426// validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox
427// were created by the command.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700428func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir, outputDir, rawCommand string) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800429 var missingOutputErrors []error
Colin Crosse52c2ac2022-03-28 17:03:35 -0700430 var incorrectOutputDirectoryErrors []error
Colin Crosse16ce362020-11-12 08:29:30 -0800431 for _, copyPair := range copies {
432 fromPath := joinPath(sandboxDir, copyPair.GetFrom())
433 fileInfo, err := os.Stat(fromPath)
434 if err != nil {
435 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath))
436 continue
437 }
438 if fileInfo.IsDir() {
439 missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath))
440 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700441
442 toPath := copyPair.GetTo()
443 if rel, err := filepath.Rel(outputDir, toPath); err != nil {
444 return err
445 } else if strings.HasPrefix(rel, "../") {
446 incorrectOutputDirectoryErrors = append(incorrectOutputDirectoryErrors,
447 fmt.Errorf("%s is not under %s", toPath, outputDir))
448 }
Colin Crosse16ce362020-11-12 08:29:30 -0800449 }
Colin Crosse52c2ac2022-03-28 17:03:35 -0700450
Steven Morelandee975ee2023-03-28 22:39:00 +0000451 const maxErrors = 25
Colin Crosse52c2ac2022-03-28 17:03:35 -0700452
453 if len(incorrectOutputDirectoryErrors) > 0 {
454 errorMessage := ""
455 more := 0
456 if len(incorrectOutputDirectoryErrors) > maxErrors {
457 more = len(incorrectOutputDirectoryErrors) - maxErrors
458 incorrectOutputDirectoryErrors = incorrectOutputDirectoryErrors[:maxErrors]
459 }
460
461 for _, err := range incorrectOutputDirectoryErrors {
462 errorMessage += err.Error() + "\n"
463 }
464 if more > 0 {
465 errorMessage += fmt.Sprintf("...%v more", more)
466 }
467
468 return errors.New(errorMessage)
469 }
470
471 if len(missingOutputErrors) > 0 {
472 // find all created files for making a more informative error message
473 createdFiles := findAllFilesUnder(sandboxDir)
474
475 // build error message
476 errorMessage := "mismatch between declared and actual outputs\n"
477 errorMessage += "in sbox command(" + rawCommand + ")\n\n"
478 errorMessage += "in sandbox " + sandboxDir + ",\n"
479 errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors))
480 for _, missingOutputError := range missingOutputErrors {
481 errorMessage += " " + missingOutputError.Error() + "\n"
482 }
483 if len(createdFiles) < 1 {
484 errorMessage += "created 0 files."
485 } else {
486 errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles))
487 creationMessages := createdFiles
488 if len(creationMessages) > maxErrors {
489 creationMessages = creationMessages[:maxErrors]
490 creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxErrors))
491 }
492 for _, creationMessage := range creationMessages {
493 errorMessage += " " + creationMessage + "\n"
494 }
495 }
496
497 return errors.New(errorMessage)
498 }
499
500 return nil
Colin Crosse16ce362020-11-12 08:29:30 -0800501}
502
Colin Crosse52c2ac2022-03-28 17:03:35 -0700503type existsType bool
504
505const (
506 requireFromExists existsType = false
507 allowFromNotExists = true
508)
509
510type writeType bool
511
512const (
513 alwaysWrite writeType = false
514 onlyWriteIfChanged = true
515)
516
517// copyFiles copies files in or out of the sandbox. If exists is allowFromNotExists then errors
518// caused by a from path not existing are ignored. If write is onlyWriteIfChanged then the output
519// file is compared to the input file and not written to if it is the same, avoiding updating
520// the timestamp.
521func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, exists existsType, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800522 for _, copyPair := range copies {
523 fromPath := joinPath(fromDir, copyPair.GetFrom())
524 toPath := joinPath(toDir, copyPair.GetTo())
Colin Crosse52c2ac2022-03-28 17:03:35 -0700525 err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), exists, write)
Colin Crosse16ce362020-11-12 08:29:30 -0800526 if err != nil {
527 return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err)
528 }
529 }
530 return nil
531}
532
Colin Crossfc2d8422021-04-15 18:36:42 -0700533// copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the
Colin Crosse52c2ac2022-03-28 17:03:35 -0700534// permissions. If exists is allowFromNotExists it returns nil if the from path doesn't exist.
535// 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 +0000536// if it is the same, avoiding updating the timestamp. If from is a symlink, the symlink itself
537// will be copied, instead of what it points to.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700538func copyOneFile(from string, to string, forceExecutable bool, exists existsType,
539 write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800540 err := os.MkdirAll(filepath.Dir(to), 0777)
541 if err != nil {
542 return err
543 }
544
Cole Faustca355af2023-12-14 23:38:38 +0000545 stat, err := os.Lstat(from)
Colin Crosse16ce362020-11-12 08:29:30 -0800546 if err != nil {
Colin Crosse52c2ac2022-03-28 17:03:35 -0700547 if os.IsNotExist(err) && exists == allowFromNotExists {
Colin Crossfc2d8422021-04-15 18:36:42 -0700548 return nil
549 }
Colin Crosse16ce362020-11-12 08:29:30 -0800550 return err
551 }
552
Cole Faustca355af2023-12-14 23:38:38 +0000553 if stat.Mode()&fs.ModeSymlink != 0 {
554 linkTarget, err := os.Readlink(from)
555 if err != nil {
556 return err
557 }
558 if write == onlyWriteIfChanged {
559 toLinkTarget, err := os.Readlink(to)
560 if err == nil && toLinkTarget == linkTarget {
561 return nil
562 }
563 }
564 err = os.Remove(to)
565 if err != nil && !os.IsNotExist(err) {
566 return err
567 }
568
569 return os.Symlink(linkTarget, to)
570 }
571
Colin Crosse16ce362020-11-12 08:29:30 -0800572 perm := stat.Mode()
Colin Crossfc2d8422021-04-15 18:36:42 -0700573 if forceExecutable {
Colin Cross859dfd92020-11-30 20:12:47 -0800574 perm = perm | 0100 // u+x
575 }
Colin Crosse16ce362020-11-12 08:29:30 -0800576
Colin Crosse52c2ac2022-03-28 17:03:35 -0700577 if write == onlyWriteIfChanged && filesHaveSameContents(from, to) {
578 return nil
579 }
580
Colin Crosse16ce362020-11-12 08:29:30 -0800581 in, err := os.Open(from)
582 if err != nil {
583 return err
584 }
585 defer in.Close()
586
Colin Cross607c0b72021-03-31 12:54:06 -0700587 // Remove the target before copying. In most cases the file won't exist, but if there are
588 // duplicate copy rules for a file and the source file was read-only the second copy could
589 // fail.
590 err = os.Remove(to)
591 if err != nil && !os.IsNotExist(err) {
592 return err
593 }
594
Colin Crosse16ce362020-11-12 08:29:30 -0800595 out, err := os.Create(to)
596 if err != nil {
597 return err
598 }
599 defer func() {
600 out.Close()
601 if err != nil {
602 os.Remove(to)
603 }
604 }()
605
606 _, err = io.Copy(out, in)
607 if err != nil {
608 return err
609 }
610
611 if err = out.Close(); err != nil {
612 return err
613 }
614
615 if err = os.Chmod(to, perm); err != nil {
616 return err
617 }
618
619 return nil
620}
621
Colin Crosse55bd422021-03-23 13:44:30 -0700622// copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files
623// listed into the sandbox.
624func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error {
625 for _, rspFile := range rspFiles {
626 err := copyOneRspFile(rspFile, toDir, toDirInSandbox)
627 if err != nil {
628 return err
629 }
630 }
631 return nil
632}
633
634// copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files
635// listed into the sandbox.
636func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error {
637 in, err := os.Open(rspFile.GetFile())
638 if err != nil {
639 return err
640 }
641 defer in.Close()
642
643 files, err := response.ReadRspFile(in)
644 if err != nil {
645 return err
646 }
647
648 for i, from := range files {
649 // Convert the real path of the input file into the path inside the sandbox using the
650 // path mappings.
651 to := applyPathMappings(rspFile.PathMappings, from)
652
653 // Copy the file into the sandbox.
Colin Crosse52c2ac2022-03-28 17:03:35 -0700654 err := copyOneFile(from, joinPath(toDir, to), false, requireFromExists, alwaysWrite)
Colin Crosse55bd422021-03-23 13:44:30 -0700655 if err != nil {
656 return err
657 }
658
659 // Rewrite the name in the list of files to be relative to the sandbox directory.
660 files[i] = joinPath(toDirInSandbox, to)
661 }
662
663 // Convert the real path of the rsp file into the path inside the sandbox using the path
664 // mappings.
665 outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile()))
666
667 err = os.MkdirAll(filepath.Dir(outRspFile), 0777)
668 if err != nil {
669 return err
670 }
671
672 out, err := os.Create(outRspFile)
673 if err != nil {
674 return err
675 }
676 defer out.Close()
677
678 // Write the rsp file with converted paths into the sandbox.
679 err = response.WriteRspFile(out, files)
680 if err != nil {
681 return err
682 }
683
684 return nil
685}
686
687// applyPathMappings takes a list of path mappings and a path, and returns the path with the first
688// matching path mapping applied. If the path does not match any of the path mappings then it is
689// returned unmodified.
690func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string {
691 for _, mapping := range pathMappings {
692 if strings.HasPrefix(path, mapping.GetFrom()+"/") {
693 return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/"))
694 }
695 }
696 return path
697}
698
Colin Crosse16ce362020-11-12 08:29:30 -0800699// moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted
700// to moving files where the source and destination are in the same filesystem. This is OK for
Colin Crosse52c2ac2022-03-28 17:03:35 -0700701// sbox because the temporary directory is inside the out directory. If write is onlyWriteIfChanged
702// then the output file is compared to the input file and not written to if it is the same, avoiding
703// updating the timestamp. Otherwise it always updates the timestamp of the new file.
704func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string, write writeType) error {
Colin Crosse16ce362020-11-12 08:29:30 -0800705 for _, copyPair := range copies {
706 fromPath := joinPath(fromDir, copyPair.GetFrom())
707 toPath := joinPath(toDir, copyPair.GetTo())
708 err := os.MkdirAll(filepath.Dir(toPath), 0777)
709 if err != nil {
710 return err
711 }
712
Colin Crosse52c2ac2022-03-28 17:03:35 -0700713 if write == onlyWriteIfChanged && filesHaveSameContents(fromPath, toPath) {
714 continue
715 }
716
Colin Crosse16ce362020-11-12 08:29:30 -0800717 err = os.Rename(fromPath, toPath)
Jeff Gaston8a88db52017-11-06 13:33:14 -0800718 if err != nil {
719 return err
720 }
Colin Crossd1c1e6f2019-03-29 13:54:39 -0700721
722 // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
723 // files with old timestamps).
724 now := time.Now()
Colin Crosse16ce362020-11-12 08:29:30 -0800725 err = os.Chtimes(toPath, now, now)
Jeff Gastonefc1b412017-03-29 17:29:06 -0700726 if err != nil {
727 return err
728 }
729 }
Jeff Gastonefc1b412017-03-29 17:29:06 -0700730 return nil
731}
Colin Crosse16ce362020-11-12 08:29:30 -0800732
Colin Crosse52c2ac2022-03-28 17:03:35 -0700733// clearOutputDirectory removes all files in the output directory if write is alwaysWrite, or
734// any files not listed in copies if write is onlyWriteIfChanged
735func clearOutputDirectory(copies []*sbox_proto.Copy, outputDir string, write writeType) error {
736 if outputDir == "" {
737 return fmt.Errorf("output directory must be set")
738 }
739
740 if write == alwaysWrite {
741 // When writing all the output files remove the whole output directory
742 return os.RemoveAll(outputDir)
743 }
744
745 outputFiles := make(map[string]bool, len(copies))
746 for _, copyPair := range copies {
747 outputFiles[copyPair.GetTo()] = true
748 }
749
750 existingFiles := findAllFilesUnder(outputDir)
751 for _, existingFile := range existingFiles {
752 fullExistingFile := filepath.Join(outputDir, existingFile)
753 if !outputFiles[fullExistingFile] {
754 err := os.Remove(fullExistingFile)
755 if err != nil {
756 return fmt.Errorf("failed to remove obsolete output file %s: %w", fullExistingFile, err)
757 }
758 }
759 }
760
761 return nil
762}
763
Colin Crosse16ce362020-11-12 08:29:30 -0800764// Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory
765// to an output file.
766func rewriteDepFiles(ins []string, out string) error {
767 var mergedDeps []string
768 for _, in := range ins {
769 data, err := ioutil.ReadFile(in)
770 if err != nil {
771 return err
772 }
773
774 deps, err := makedeps.Parse(in, bytes.NewBuffer(data))
775 if err != nil {
776 return err
777 }
778 mergedDeps = append(mergedDeps, deps.Inputs...)
779 }
780
781 deps := makedeps.Deps{
782 // Ninja doesn't care what the output file is, so we can use any string here.
783 Output: "outputfile",
784 Inputs: mergedDeps,
785 }
786
787 // Make the directory for the output depfile in case it is in a different directory
788 // than any of the output files.
789 outDir := filepath.Dir(out)
790 err := os.MkdirAll(outDir, 0777)
791 if err != nil {
792 return fmt.Errorf("failed to create %q: %w", outDir, err)
793 }
794
795 return ioutil.WriteFile(out, deps.Print(), 0666)
796}
797
798// joinPath wraps filepath.Join but returns file without appending to dir if file is
799// absolute.
800func joinPath(dir, file string) string {
801 if filepath.IsAbs(file) {
802 return file
803 }
804 return filepath.Join(dir, file)
805}
Colin Crossc590ec42021-03-11 17:20:02 -0800806
Colin Crosse52c2ac2022-03-28 17:03:35 -0700807// filesHaveSameContents compares the contents if two files, returning true if they are the same
808// and returning false if they are different or any errors occur.
809func filesHaveSameContents(a, b string) bool {
810 // Compare the sizes of the two files
811 statA, err := os.Stat(a)
812 if err != nil {
813 return false
814 }
815 statB, err := os.Stat(b)
816 if err != nil {
817 return false
818 }
819
820 if statA.Size() != statB.Size() {
821 return false
822 }
823
824 // Open the two files
825 fileA, err := os.Open(a)
826 if err != nil {
827 return false
828 }
829 defer fileA.Close()
Colin Crossfa8e9cc2022-04-12 17:26:58 -0700830 fileB, err := os.Open(b)
Colin Crosse52c2ac2022-03-28 17:03:35 -0700831 if err != nil {
832 return false
833 }
834 defer fileB.Close()
835
836 // Compare the files 1MB at a time
837 const bufSize = 1 * 1024 * 1024
838 bufA := make([]byte, bufSize)
839 bufB := make([]byte, bufSize)
840
841 remain := statA.Size()
842 for remain > 0 {
843 toRead := int64(bufSize)
844 if toRead > remain {
845 toRead = remain
846 }
847
848 _, err = io.ReadFull(fileA, bufA[:toRead])
849 if err != nil {
850 return false
851 }
852 _, err = io.ReadFull(fileB, bufB[:toRead])
853 if err != nil {
854 return false
855 }
856
857 if bytes.Compare(bufA[:toRead], bufB[:toRead]) != 0 {
858 return false
859 }
860
861 remain -= toRead
862 }
863
864 return true
865}
866
Colin Crossc590ec42021-03-11 17:20:02 -0800867func makeAbsPathEnv(pathEnv string) (string, error) {
868 pathEnvElements := filepath.SplitList(pathEnv)
869 for i, p := range pathEnvElements {
870 if !filepath.IsAbs(p) {
871 absPath, err := filepath.Abs(p)
872 if err != nil {
873 return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err)
874 }
875 pathEnvElements[i] = absPath
876 }
877 }
878 return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil
879}