| // Copyright 2017 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "crypto/sha1" |
| "encoding/hex" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "time" |
| |
| "android/soong/cmd/sbox/sbox_proto" |
| "android/soong/makedeps" |
| "android/soong/response" |
| |
| "google.golang.org/protobuf/encoding/prototext" |
| ) |
| |
| var ( |
| sandboxesRoot string |
| manifestFile string |
| keepOutDir bool |
| ) |
| |
| const ( |
| depFilePlaceholder = "__SBOX_DEPFILE__" |
| sandboxDirPlaceholder = "__SBOX_SANDBOX_DIR__" |
| ) |
| |
| func init() { |
| flag.StringVar(&sandboxesRoot, "sandbox-path", "", |
| "root of temp directory to put the sandbox into") |
| flag.StringVar(&manifestFile, "manifest", "", |
| "textproto manifest describing the sandboxed command(s)") |
| flag.BoolVar(&keepOutDir, "keep-out-dir", false, |
| "whether to keep the sandbox directory when done") |
| } |
| |
| func usageViolation(violation string) { |
| if violation != "" { |
| fmt.Fprintf(os.Stderr, "Usage error: %s.\n\n", violation) |
| } |
| |
| fmt.Fprintf(os.Stderr, |
| "Usage: sbox --manifest <manifest> --sandbox-path <sandboxPath>\n") |
| |
| flag.PrintDefaults() |
| |
| os.Exit(1) |
| } |
| |
| func main() { |
| flag.Usage = func() { |
| usageViolation("") |
| } |
| flag.Parse() |
| |
| error := run() |
| if error != nil { |
| fmt.Fprintln(os.Stderr, error) |
| os.Exit(1) |
| } |
| } |
| |
| func findAllFilesUnder(root string) (paths []string) { |
| paths = []string{} |
| filepath.Walk(root, func(path string, info os.FileInfo, err error) error { |
| if !info.IsDir() { |
| relPath, err := filepath.Rel(root, path) |
| if err != nil { |
| // couldn't find relative path from ancestor? |
| panic(err) |
| } |
| paths = append(paths, relPath) |
| } |
| return nil |
| }) |
| return paths |
| } |
| |
| func run() error { |
| if manifestFile == "" { |
| usageViolation("--manifest <manifest> is required and must be non-empty") |
| } |
| if sandboxesRoot == "" { |
| // In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR, |
| // and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so |
| // the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable |
| // However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it) |
| // and by passing it as a parameter we don't need to duplicate its value |
| usageViolation("--sandbox-path <sandboxPath> is required and must be non-empty") |
| } |
| |
| manifest, err := readManifest(manifestFile) |
| |
| if len(manifest.Commands) == 0 { |
| return fmt.Errorf("at least one commands entry is required in %q", manifestFile) |
| } |
| |
| // setup sandbox directory |
| err = os.MkdirAll(sandboxesRoot, 0777) |
| if err != nil { |
| return fmt.Errorf("failed to create %q: %w", sandboxesRoot, err) |
| } |
| |
| // This tool assumes that there are no two concurrent runs with the same |
| // manifestFile. It should therefore be safe to use the hash of the |
| // manifestFile as the temporary directory name. We do this because it |
| // makes the temporary directory name deterministic. There are some |
| // tools that embed the name of the temporary output in the output, and |
| // they otherwise cause non-determinism, which then poisons actions |
| // depending on this one. |
| hash := sha1.New() |
| hash.Write([]byte(manifestFile)) |
| tempDir := filepath.Join(sandboxesRoot, "sbox", hex.EncodeToString(hash.Sum(nil))) |
| |
| err = os.RemoveAll(tempDir) |
| if err != nil { |
| return err |
| } |
| err = os.MkdirAll(tempDir, 0777) |
| if err != nil { |
| return fmt.Errorf("failed to create temporary dir in %q: %w", sandboxesRoot, err) |
| } |
| |
| // In the common case, the following line of code is what removes the sandbox |
| // If a fatal error occurs (such as if our Go process is killed unexpectedly), |
| // then at the beginning of the next build, Soong will wipe the temporary |
| // directory. |
| defer func() { |
| // in some cases we decline to remove the temp dir, to facilitate debugging |
| if !keepOutDir { |
| os.RemoveAll(tempDir) |
| } |
| }() |
| |
| // If there is more than one command in the manifest use a separate directory for each one. |
| useSubDir := len(manifest.Commands) > 1 |
| var commandDepFiles []string |
| |
| for i, command := range manifest.Commands { |
| localTempDir := tempDir |
| if useSubDir { |
| localTempDir = filepath.Join(localTempDir, strconv.Itoa(i)) |
| } |
| depFile, err := runCommand(command, localTempDir) |
| if err != nil { |
| // Running the command failed, keep the temporary output directory around in |
| // case a user wants to inspect it for debugging purposes. Soong will delete |
| // it at the beginning of the next build anyway. |
| keepOutDir = true |
| return err |
| } |
| if depFile != "" { |
| commandDepFiles = append(commandDepFiles, depFile) |
| } |
| } |
| |
| outputDepFile := manifest.GetOutputDepfile() |
| if len(commandDepFiles) > 0 && outputDepFile == "" { |
| return fmt.Errorf("Sandboxed commands used %s but output depfile is not set in manifest file", |
| depFilePlaceholder) |
| } |
| |
| if outputDepFile != "" { |
| // Merge the depfiles from each command in the manifest to a single output depfile. |
| err = rewriteDepFiles(commandDepFiles, outputDepFile) |
| if err != nil { |
| return fmt.Errorf("failed merging depfiles: %w", err) |
| } |
| } |
| |
| return nil |
| } |
| |
| // readManifest reads an sbox manifest from a textproto file. |
| func readManifest(file string) (*sbox_proto.Manifest, error) { |
| manifestData, err := ioutil.ReadFile(file) |
| if err != nil { |
| return nil, fmt.Errorf("error reading manifest %q: %w", file, err) |
| } |
| |
| manifest := sbox_proto.Manifest{} |
| |
| err = prototext.Unmarshal(manifestData, &manifest) |
| if err != nil { |
| return nil, fmt.Errorf("error parsing manifest %q: %w", file, err) |
| } |
| |
| return &manifest, nil |
| } |
| |
| // runCommand runs a single command from a manifest. If the command references the |
| // __SBOX_DEPFILE__ placeholder it returns the name of the depfile that was used. |
| func runCommand(command *sbox_proto.Command, tempDir string) (depFile string, err error) { |
| rawCommand := command.GetCommand() |
| if rawCommand == "" { |
| return "", fmt.Errorf("command is required") |
| } |
| |
| pathToTempDirInSbox := tempDir |
| if command.GetChdir() { |
| pathToTempDirInSbox = "." |
| } |
| |
| err = os.MkdirAll(tempDir, 0777) |
| if err != nil { |
| return "", fmt.Errorf("failed to create %q: %w", tempDir, err) |
| } |
| |
| // Copy in any files specified by the manifest. |
| err = copyFiles(command.CopyBefore, "", tempDir, false) |
| if err != nil { |
| return "", err |
| } |
| err = copyRspFiles(command.RspFiles, tempDir, pathToTempDirInSbox) |
| if err != nil { |
| return "", err |
| } |
| |
| if strings.Contains(rawCommand, depFilePlaceholder) { |
| depFile = filepath.Join(pathToTempDirInSbox, "deps.d") |
| rawCommand = strings.Replace(rawCommand, depFilePlaceholder, depFile, -1) |
| } |
| |
| if strings.Contains(rawCommand, sandboxDirPlaceholder) { |
| rawCommand = strings.Replace(rawCommand, sandboxDirPlaceholder, pathToTempDirInSbox, -1) |
| } |
| |
| // Emulate ninja's behavior of creating the directories for any output files before |
| // running the command. |
| err = makeOutputDirs(command.CopyAfter, tempDir) |
| if err != nil { |
| return "", err |
| } |
| |
| cmd := exec.Command("bash", "-c", rawCommand) |
| buf := &bytes.Buffer{} |
| cmd.Stdin = os.Stdin |
| cmd.Stdout = buf |
| cmd.Stderr = buf |
| |
| if command.GetChdir() { |
| cmd.Dir = tempDir |
| path := os.Getenv("PATH") |
| absPath, err := makeAbsPathEnv(path) |
| if err != nil { |
| return "", err |
| } |
| err = os.Setenv("PATH", absPath) |
| if err != nil { |
| return "", fmt.Errorf("Failed to update PATH: %w", err) |
| } |
| } |
| err = cmd.Run() |
| |
| if err != nil { |
| // The command failed, do a best effort copy of output files out of the sandbox. This is |
| // especially useful for linters with baselines that print an error message on failure |
| // with a command to copy the output lint errors to the new baseline. Use a copy instead of |
| // a move to leave the sandbox intact for manual inspection |
| copyFiles(command.CopyAfter, tempDir, "", true) |
| } |
| |
| // If the command was executed but failed with an error, print a debugging message before |
| // the command's output so it doesn't scroll the real error message off the screen. |
| if exit, ok := err.(*exec.ExitError); ok && !exit.Success() { |
| fmt.Fprintf(os.Stderr, |
| "The failing command was run inside an sbox sandbox in temporary directory\n"+ |
| "%s\n"+ |
| "The failing command line was:\n"+ |
| "%s\n", |
| tempDir, rawCommand) |
| } |
| |
| // Write the command's combined stdout/stderr. |
| os.Stdout.Write(buf.Bytes()) |
| |
| if err != nil { |
| return "", err |
| } |
| |
| missingOutputErrors := validateOutputFiles(command.CopyAfter, tempDir) |
| |
| if len(missingOutputErrors) > 0 { |
| // find all created files for making a more informative error message |
| createdFiles := findAllFilesUnder(tempDir) |
| |
| // build error message |
| errorMessage := "mismatch between declared and actual outputs\n" |
| errorMessage += "in sbox command(" + rawCommand + ")\n\n" |
| errorMessage += "in sandbox " + tempDir + ",\n" |
| errorMessage += fmt.Sprintf("failed to create %v files:\n", len(missingOutputErrors)) |
| for _, missingOutputError := range missingOutputErrors { |
| errorMessage += " " + missingOutputError.Error() + "\n" |
| } |
| if len(createdFiles) < 1 { |
| errorMessage += "created 0 files." |
| } else { |
| errorMessage += fmt.Sprintf("did create %v files:\n", len(createdFiles)) |
| creationMessages := createdFiles |
| maxNumCreationLines := 10 |
| if len(creationMessages) > maxNumCreationLines { |
| creationMessages = creationMessages[:maxNumCreationLines] |
| creationMessages = append(creationMessages, fmt.Sprintf("...%v more", len(createdFiles)-maxNumCreationLines)) |
| } |
| for _, creationMessage := range creationMessages { |
| errorMessage += " " + creationMessage + "\n" |
| } |
| } |
| |
| return "", errors.New(errorMessage) |
| } |
| // the created files match the declared files; now move them |
| err = moveFiles(command.CopyAfter, tempDir, "") |
| |
| return depFile, nil |
| } |
| |
| // makeOutputDirs creates directories in the sandbox dir for every file that has a rule to be copied |
| // out of the sandbox. This emulate's Ninja's behavior of creating directories for output files |
| // so that the tools don't have to. |
| func makeOutputDirs(copies []*sbox_proto.Copy, sandboxDir string) error { |
| for _, copyPair := range copies { |
| dir := joinPath(sandboxDir, filepath.Dir(copyPair.GetFrom())) |
| err := os.MkdirAll(dir, 0777) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // validateOutputFiles verifies that all files that have a rule to be copied out of the sandbox |
| // were created by the command. |
| func validateOutputFiles(copies []*sbox_proto.Copy, sandboxDir string) []error { |
| var missingOutputErrors []error |
| for _, copyPair := range copies { |
| fromPath := joinPath(sandboxDir, copyPair.GetFrom()) |
| fileInfo, err := os.Stat(fromPath) |
| if err != nil { |
| missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: does not exist", fromPath)) |
| continue |
| } |
| if fileInfo.IsDir() { |
| missingOutputErrors = append(missingOutputErrors, fmt.Errorf("%s: not a file", fromPath)) |
| } |
| } |
| return missingOutputErrors |
| } |
| |
| // copyFiles copies files in or out of the sandbox. If allowFromNotExists is true then errors |
| // caused by a from path not existing are ignored. |
| func copyFiles(copies []*sbox_proto.Copy, fromDir, toDir string, allowFromNotExists bool) error { |
| for _, copyPair := range copies { |
| fromPath := joinPath(fromDir, copyPair.GetFrom()) |
| toPath := joinPath(toDir, copyPair.GetTo()) |
| err := copyOneFile(fromPath, toPath, copyPair.GetExecutable(), allowFromNotExists) |
| if err != nil { |
| return fmt.Errorf("error copying %q to %q: %w", fromPath, toPath, err) |
| } |
| } |
| return nil |
| } |
| |
| // copyOneFile copies a file and its permissions. If forceExecutable is true it adds u+x to the |
| // permissions. If allowFromNotExists is true it returns nil if the from path doesn't exist. |
| func copyOneFile(from string, to string, forceExecutable, allowFromNotExists bool) error { |
| err := os.MkdirAll(filepath.Dir(to), 0777) |
| if err != nil { |
| return err |
| } |
| |
| stat, err := os.Stat(from) |
| if err != nil { |
| if os.IsNotExist(err) && allowFromNotExists { |
| return nil |
| } |
| return err |
| } |
| |
| perm := stat.Mode() |
| if forceExecutable { |
| perm = perm | 0100 // u+x |
| } |
| |
| in, err := os.Open(from) |
| if err != nil { |
| return err |
| } |
| defer in.Close() |
| |
| // Remove the target before copying. In most cases the file won't exist, but if there are |
| // duplicate copy rules for a file and the source file was read-only the second copy could |
| // fail. |
| err = os.Remove(to) |
| if err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| |
| out, err := os.Create(to) |
| if err != nil { |
| return err |
| } |
| defer func() { |
| out.Close() |
| if err != nil { |
| os.Remove(to) |
| } |
| }() |
| |
| _, err = io.Copy(out, in) |
| if err != nil { |
| return err |
| } |
| |
| if err = out.Close(); err != nil { |
| return err |
| } |
| |
| if err = os.Chmod(to, perm); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // copyRspFiles copies rsp files into the sandbox with path mappings, and also copies the files |
| // listed into the sandbox. |
| func copyRspFiles(rspFiles []*sbox_proto.RspFile, toDir, toDirInSandbox string) error { |
| for _, rspFile := range rspFiles { |
| err := copyOneRspFile(rspFile, toDir, toDirInSandbox) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // copyOneRspFiles copies an rsp file into the sandbox with path mappings, and also copies the files |
| // listed into the sandbox. |
| func copyOneRspFile(rspFile *sbox_proto.RspFile, toDir, toDirInSandbox string) error { |
| in, err := os.Open(rspFile.GetFile()) |
| if err != nil { |
| return err |
| } |
| defer in.Close() |
| |
| files, err := response.ReadRspFile(in) |
| if err != nil { |
| return err |
| } |
| |
| for i, from := range files { |
| // Convert the real path of the input file into the path inside the sandbox using the |
| // path mappings. |
| to := applyPathMappings(rspFile.PathMappings, from) |
| |
| // Copy the file into the sandbox. |
| err := copyOneFile(from, joinPath(toDir, to), false, false) |
| if err != nil { |
| return err |
| } |
| |
| // Rewrite the name in the list of files to be relative to the sandbox directory. |
| files[i] = joinPath(toDirInSandbox, to) |
| } |
| |
| // Convert the real path of the rsp file into the path inside the sandbox using the path |
| // mappings. |
| outRspFile := joinPath(toDir, applyPathMappings(rspFile.PathMappings, rspFile.GetFile())) |
| |
| err = os.MkdirAll(filepath.Dir(outRspFile), 0777) |
| if err != nil { |
| return err |
| } |
| |
| out, err := os.Create(outRspFile) |
| if err != nil { |
| return err |
| } |
| defer out.Close() |
| |
| // Write the rsp file with converted paths into the sandbox. |
| err = response.WriteRspFile(out, files) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // applyPathMappings takes a list of path mappings and a path, and returns the path with the first |
| // matching path mapping applied. If the path does not match any of the path mappings then it is |
| // returned unmodified. |
| func applyPathMappings(pathMappings []*sbox_proto.PathMapping, path string) string { |
| for _, mapping := range pathMappings { |
| if strings.HasPrefix(path, mapping.GetFrom()+"/") { |
| return joinPath(mapping.GetTo()+"/", strings.TrimPrefix(path, mapping.GetFrom()+"/")) |
| } |
| } |
| return path |
| } |
| |
| // moveFiles moves files specified by a set of copy rules. It uses os.Rename, so it is restricted |
| // to moving files where the source and destination are in the same filesystem. This is OK for |
| // sbox because the temporary directory is inside the out directory. It updates the timestamp |
| // of the new file. |
| func moveFiles(copies []*sbox_proto.Copy, fromDir, toDir string) error { |
| for _, copyPair := range copies { |
| fromPath := joinPath(fromDir, copyPair.GetFrom()) |
| toPath := joinPath(toDir, copyPair.GetTo()) |
| err := os.MkdirAll(filepath.Dir(toPath), 0777) |
| if err != nil { |
| return err |
| } |
| |
| err = os.Rename(fromPath, toPath) |
| if err != nil { |
| return err |
| } |
| |
| // Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract |
| // files with old timestamps). |
| now := time.Now() |
| err = os.Chtimes(toPath, now, now) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // Rewrite one or more depfiles so that it doesn't include the (randomized) sandbox directory |
| // to an output file. |
| func rewriteDepFiles(ins []string, out string) error { |
| var mergedDeps []string |
| for _, in := range ins { |
| data, err := ioutil.ReadFile(in) |
| if err != nil { |
| return err |
| } |
| |
| deps, err := makedeps.Parse(in, bytes.NewBuffer(data)) |
| if err != nil { |
| return err |
| } |
| mergedDeps = append(mergedDeps, deps.Inputs...) |
| } |
| |
| deps := makedeps.Deps{ |
| // Ninja doesn't care what the output file is, so we can use any string here. |
| Output: "outputfile", |
| Inputs: mergedDeps, |
| } |
| |
| // Make the directory for the output depfile in case it is in a different directory |
| // than any of the output files. |
| outDir := filepath.Dir(out) |
| err := os.MkdirAll(outDir, 0777) |
| if err != nil { |
| return fmt.Errorf("failed to create %q: %w", outDir, err) |
| } |
| |
| return ioutil.WriteFile(out, deps.Print(), 0666) |
| } |
| |
| // joinPath wraps filepath.Join but returns file without appending to dir if file is |
| // absolute. |
| func joinPath(dir, file string) string { |
| if filepath.IsAbs(file) { |
| return file |
| } |
| return filepath.Join(dir, file) |
| } |
| |
| func makeAbsPathEnv(pathEnv string) (string, error) { |
| pathEnvElements := filepath.SplitList(pathEnv) |
| for i, p := range pathEnvElements { |
| if !filepath.IsAbs(p) { |
| absPath, err := filepath.Abs(p) |
| if err != nil { |
| return "", fmt.Errorf("failed to make PATH entry %q absolute: %w", p, err) |
| } |
| pathEnvElements[i] = absPath |
| } |
| } |
| return strings.Join(pathEnvElements, string(filepath.ListSeparator)), nil |
| } |